Recently I went over how WebKit lays out and renders the text of a webpage, but in doing so I skipped over the step of applying CSS.
I want to revisit that step now.
But what need is CSS filling? To me the whole point webdevs to decouple style from semantics, and in doing so it allows you to improve that style for your own needs.
Browsers have always had stylesheets, but those stylesheets weren’t always CSS or provided by the server. This is a vital step for a universal document format.
The first story starts with the
<style> elements, which constructs a CSSParserContext and wrapping StyleSheetContents (which stores the style rules) to parse bytes being downloaded.
<link> will do so once it’s notified the response is incoming, which is fetched via the same HTTP cache and network sandboxes used for the main load. And
<style> calls CSSParserContext via InlineStyleSheetOwner.
The first pass (done ahead of time of time) is to split the (fully downloaded) text into an array of “tokens”, whilst verifying the paratheses, etc are balanced. This is done using an explicit jump table, falling back to yielding “deliter” tokens for ASCII and lexes any other Unicode characters as the start of an identifier.
That array holding these is presized to an estimated length, and comment tokens are skipped before they’re added to it. An EOF token ends the scanning.
And the second pass is to gather these tokens up into a sequence of “rules” and contained “declaration lists” to add to the StyleSheetContents, for each it may optionally defer parsing a notify a registered observer.
The outer loop mainly serves to skip any whitespace (which is kept for the sake of selectors) and handle @-rules specially, which themselves are split between those expecting block and those don’t.
CSSParserTokenRange underlies the parser, scanning over blocks, values, and spaces.
|Selectors are parsed at this point too (by CSSSelectorParser), and are split into multi-level lists: comma seperated, “compound” selectors (separated by space, +, ~, or > “combinators”), and “complex” selectors. “Simple” selectors are the leaves and include things like #id,||namespace, .class, :psuedo, [property], etc.|
This operates on a slice of the token stream, and properly parsing whitespace as an operator is a little tricky.
Property values are also parsed here in order to ensure that cascade won’t override a valid with an invalid one.
The first step of parsing properties is to lookup the property’s ID in a “perfect hashmap” generated by a Perl script compiling JSON to C++ and GPerf.
The second step is to use a number of switch statements (for shorthands, properties whose value is always an keyword, or other properties) to apply the parsing logic specific to that property.
Last night I described how WebKit parses a CSS stylesheet, and now today I want to describe how those style rules are applied to HTML elements in order to compute a “style tree”. Thereby looking up all the details of how to represent the semantics of that element visually.
This task is overseen by a StyleResolver object which is called by the Document, and that’s where this tour will start.
For a first pass WebKit checks if it can reuse the results from styling another element, but only if that element passes a bunch of disqualifying tests.
This involves looking for an equivalent sibling element that has already been styled, falling back to it’s parent’s siblings.
Along with their cached styles the elements store some flags to control style sharing, which becomes critical in the face of “compound selectors”. And it tracks selectors which may invalidate style sharing results.
The next passes (called via StyleResolver, which I’ll revisit) take the tact of filtering down the possibly matching style rules as cheaply as possible. With the second pass being “ElementRuleCollector”, which looks up rules from “RuleSet”s.
By looking up selectors which match the element’s ID, classes, psuedoclasses, or tagname it knows that all rules being considered match in at least one way (caveat for “*”).
RuleSets are populated directly from the StyleSheetContents described previously.
I recall there being a third pass that used “counting bloom filters”, but it appears not to be used anymore. Still bloom filters are fascinating so let me cover it anyways.
The idea was to provide a probabilistic check that all the ancestors expected by the selector are present. It’s very much like a hashset but it embraces collisions to keep a fixed size and obtain constant-time performance.
A counting bloom filter adds a count to each bucket in order to remove previously-added values.
This is a two pass process: from the AST to “fragments” and from fragments to Machine Code, In the second pass it tracks “backtracking” targets and has dedicated registers for the inspected element, return value, etc.
However that compiler cannot handle various lesser-used CSS selector components, so if the AST to fragment compiler indicates a certain kind of failure (or the compilation cannot be done at all), it falls back to an interpretor that simply traverses the AST. This interpretor can be considered the final pass.
The compiler reportedly takes a bit over the same ammount of time as a single execution of that interpretor, but it makes later executions much faster.
Cascade & Inheritance
Once WebKit has filtered down all relevant style rules, the ElementRuleCollector has to sort them by specificity (computed by the last rule filter) falling back to line number, before the StyleResolver resolving cascade by loading all the properties into an array mapping property IDs to their values.
Then it iterates over those properties to translate them into calls to setter methods on the style object, via code compiled from JSON to C++ via Perl possibly also applying animations.
To improve the performance of CSS inheritance, the style objects splits it’s fields into the chunks of related fields. Thus allowing style objects to share those chunks until one of it’s fields have changed, because usually the data copied over from the parent or default styles do not change.