-
Notifications
You must be signed in to change notification settings - Fork 157
Design Doc: Inline CSS Imports
Matt Atterbury, October 2011
CSS @import rules each require the browser to fetch the imported file, resulting in multiple round-trips. In line with other filters this one aims to eliminate, or at least reduce, the number of GET requests by the browser, albeit at the expense of increasing the amount of data sent in each response and eliminating possible parallel GETs by the browser (though the rewriter can do the same).
- Convert a hierarchy of CSS files into a single CSS file.
- The single CSS file should be exactly equivalent to the original file hierarchy.
- Allow downstream filters to optimize the resulting single CSS file.
- Handling JS scripts that manipulate the CSS and assume anything about @import rules - these will break.
A CSS import rule take the form: @import *url* [*media*];
If the media is not provided the specified file will fetched and its contents will replace the @import rule. The optional media controls the media for which the import is effective and is processed by the browser; it takes values like all, print, or screen (or a comma-separated list thereof). Since we cannot know how the browser will process it, we will replace the @import with the file’s contents surrounded by an @media rule specifying the same media as the original @import (sort of - see the @media rule section below). If the media is exactly all then the @media rule will be omitted since it is unnecessary. If an imported file has any @import rules these will be replaced recursively.
@import url(styles.css); | div { background-color: blue; } |
@import url(styles.css) all; | div { background-color: blue; } |
@import url(styles.css) screen; | @media screen { div { background-color: blue; } } |
The Css::Parser class will be used to parse the CSS into a Stylesheet instance. Each Import instance from this will be removed then its URL loaded and parsed into another Stylesheet and its elements merged into the top-level one, and so on recursively (as a nested rewriter). The end result will be a single Stylesheet with no Import instances. When the last @import is inlined the Stylesheet will be converted back to text and emitted and the filter will be finished.
If there are any errors while parsing a CSS file then the entire process will be aborted. If we were to inline then the entire flattened CSS could be invalid, which could be different from how a browser handles imported files (it could just abort importing of the bad file for example). We also can’t leave the @import in the CSS since these must be at the start of a stylesheet.
If a 404 response is received for an imported file then that file will be ignored but processing will continue. This might not be a good idea because of this statement from the CSS 2.1 2003 specification: User agents may vary in how they handle URIs that designate unavailable or inapplicable resources.
If any other error response is received for an imported file (e.g. 502) then the process will be aborted and the CSS will be left un-inlined.
A stylesheet can reference other resources using the url( notation; if a relative URL is specified the base for absolutification is the URL of the containing CSS file. However, once an @import is inlined the base for absolutification will be the URL of the top-level CSS file. This means that all url( function arguments will have to be absolutified when the file is inlined to ensure that the correct [absolute] path is used - these can be trimmed at the same time assuming the base of the top-level CSS file, or the trimming can be left for a later filter. A stack of base URLs will have to be kept, with the new base push’d when an @import is started and pop’d when it is finished.
Successful rewrites will need to be cached by RewriteContext as a CachedResult, though note that subsequent filters might rewrite the CSS further (and change the OutputResource’s URL in the process).
Failed rewrites should be cached as well to avoid repeated doomed attempts at rewriting. The cache life might be [very] short to cater for transient errors. The cache life might be dependent on the actual error - a 404 response could mean a short life while a parse error could mean a long life.
An imported CSS file may start with an @charset rule - it must be the very first thing in the file. If an imported file starts with an @charset rule and the specified character encoding is different from the original encoding, the entire flattening process must be aborted and the original CSS left unchanged; the original encoding will be that of the top-level HTML being processed. If an @charset specifies the same character encoding as is already in use, then the @charset rule will be deleted and processing will continue as described above.
Rulesets in an imported CSS file may be contained in their own @media rule, and @media rules cannot be nested, so when I said above "surrounded by an @media rule “ that’s not technically accurate. We actually need to tag each ruleset in the parsed Stylesheet with the containing media: if the ruleset has no media then we set its media to the containing media; if it has media already we find the intersection of its media and the containing media and if that is empty we discard the ruleset otherwise we set the ruleset’s media to this intersection.
It is possible that the inlining will result in a large single CSS file, making the containing response larger thereby negatively impacting performance - the assumption is that any such impact will be more than offset by the improvement from having a single response. However we will probably have a configuration parameter that limits how big the inlined CSS can be.
It is possible that an imported file has been rewritten and cached because it was used directly somewhere else. In this case we should try to use the rewritten version rather than the original.
We should track the media tags on the @import rules and ignore a file if it can’t be used because of its media type. For example, if we process @import url(a.css) screen; and a.css starts with @import url(b.css) print; there is no point in processing b.css because it cannot possibly apply.
We should cache each flattened @import separately in case they are used multiple times (which apparently will happen automatically due to this being a nest rewriter). For example, say we have a.css and b.css and both import c.css - if we cached the flattened version of c.css while processing a.css, it would speed up the flattening of b.css later.