-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Feature Design: JIT CSS Optimization
Target Version: v1.2
Status: Draft
This document outlines the design specifications for adding an automatic CSS optimization engine to Violetear. This feature ensures that Server-Side Rendered (SSR) pages serve the absolute minimum amount of CSS required to render the content, significantly improving First Contentful Paint (FCP) and reducing network usage.
Objective
Implement a pipeline that scans the generated HTML (Document object model), identifies the CSS classes actually in use, and generates a minimal CSS subset on the fly to be inlined directly into the response.
1. Scanner Logic (violetear.markup)
We need a reliable way to traverse the Violetear component tree and extract all class names referenced by the elements.
1.1 Element.scan_classes()
- Responsibility: Recursively collect all tokens in
self.classesfrom the element and all its children. - Returns:
Set[str]to ensure uniqueness.
(Note: Basic implementation already exists in the current codebase, but needs to be formalized and tested against Component subclasses).
1.2 Document.get_used_classes()
- Responsibility: Entry point for the App to request the usage set from the entire document body.
2. Filter Logic (violetear.stylesheet)
The StyleSheet class needs a new rendering mode that accepts a filter.
2.1 StyleSheet.render_subset(used_classes: Set[str]) -> str
- Input: A set of class names (strings) found in the document.
- Logic:
- Iterate over all stored
Styleobjects. - Check the
Selectorof each style. - Inclusion Criteria:
- If the selector targets a Tag (e.g.,
body,h1), always include (global styles). - If the selector targets an ID (e.g.,
#header), always include (specific styles). - If the selector targets Classes (e.g.,
.btn.primary):- Include ONLY if all classes in the selector are present in
used_classes. - Example: Rule
.btn.primaryis kept only if bothbtnandprimaryare used.
- Include ONLY if all classes in the selector are present in
- If the selector targets a Tag (e.g.,
- Animations: If a style is included and it uses an
@keyframesanimation, that animation definition must also be included in the output.
- Iterate over all stored
- Output: A string containing only the valid CSS rules.
3. Server-Side Integration (violetear.app.App)
The App class handles the request/response lifecycle and decides how to serve the CSS.
3.1 Routing Logic Update
We will update the route wrapper to support a mode switch (e.g., jit=True or default behavior for non-PWA routes).
Workflow:
- Execute user view function -> Get
Document. - Scan: Call
doc.get_used_classes()to get the set of active tokens. - Process Assets: Iterate through
doc.head.styles.- If the resource is a
StyleSheetobject (not a string URL):- Call
sheet.render_subset(used_classes). - Replace the
<link>tag in the header with a<style>tag containing the optimized CSS string.
- Call
- If the resource is a URL (CDN/Static file):
- Leave as is (cannot optimize external resources).
- If the resource is a
- Return: Send the optimized HTML response.
3.2 Caching Considerations (Future)
- For high-traffic production, we might want to cache the generated CSS string based on a hash of the
used_classesset to avoid re-filtering the stylesheet on every request.
4. Usage Example
No changes required in user code! The optimization happens transparently.
# User defines a massive theme (e.g. Atomic/Tailwind preset with 10,000 rules)
app.add_style(Atomic())
@app.route("/")
def home():
# User uses only 2 classes
return Document().add(Element("div", classes="text-xl text-red"))
# RESULT:
# The browser receives an HTML with a <style> block containing
# ONLY the rules for .text-xl and .text-red.
# The other 9,998 rules are discarded.5. Task List
- Add
render_subsetmethod toStyleSheet. - Ensure
render_subsetcorrectly handles dependency resolution (e.g., styles using@keyframes). - Update
App.routeto perform the scan-and-inline process forDocumentresponses. - Add unit tests verifying that unused classes are indeed stripped from the output.