How Chromium Calculates CSS in Style Recalculation
This article explains the mechanics behind Chromium's style recalculation phase, detailing how the Blink rendering engine processes CSS rules and applies them to the Document Object Model (DOM). It covers the exact steps Chromium takes, starting from CSS rule indexing and selector matching, moving through cascade resolution, and ending with the generation of the final computed styles required for page layout.
Rule Indexing and Bucketing
Before matching styles to any DOM elements, Chromium's Blink engine
organizes parsed CSS rules into an optimized data structure called a
RuleSet. Instead of comparing every CSS rule in a
stylesheet against every DOM node, Chromium groups rules into specific
"buckets" (hash maps) based on their rightmost selector, known as the
key selector.
There are separate buckets for IDs, class names, tag names, and
attributes, plus a universal bucket for general rules. For example, in
the selector .nav > ul li.active, the key selector is
.active, so the rule is placed into the active
class bucket. This pre-processing drastically reduces the number of
rules the engine must evaluate for any given element.
DOM Traversal and Selector Matching
When a change triggers the style recalculation phase, Chromium
traverses the DOM tree in a top-down manner. For each element that needs
its styles calculated, Chromium instantiates an
ElementRuleCollector.
The collector inspects the element's ID, classes, attributes, and tag
name, and pulls only the relevant candidate rules from the corresponding
RuleSet buckets. Chromium then performs selector matching
on these candidates. To maximize performance, it evaluates complex
selectors from right to left. It starts at the target element (matching
the key selector) and walks up or across the DOM tree to verify parent,
ancestor, or sibling combinators. If a rule fails to match at any point,
the engine immediately abandons that rule and moves to the next
candidate.
Resolving the Cascade
Once all matching CSS rules for an element are collected, Chromium
must determine which declarations win when property conflicts arise. The
ElementRuleCollector sorts the matched rules based on the
standard CSS cascade criteria:
- Origin and Importance: The engine separates
user-agent styles (browser defaults), user styles, and author styles
(website CSS), while prioritizing rules marked with
!important. - Cascade Layers: Rules are ordered according to any
defined
@layerrules. - Specificity: Chromium calculates a specificity score based on the components of the selector (IDs outweigh classes, which outweigh tags).
- Order of Appearance: If specificity is tied, rules declared later in the stylesheet or document override earlier ones.
Generating the ComputedStyle
After sorting the matched rules, Chromium uses a component called the
StyleBuilder to take the winning property declarations and
create an immutable ComputedStyle object for the
element.
During this step, the engine resolves relative values into absolute,
usable values. For instance, relative units like em,
rem, and vh are converted into concrete pixel
values based on the parent element, root element, or viewport
dimensions.
CSS inheritance is also finalized during this step. Because Chromium
traverses the DOM from top to bottom, the parent element's
ComputedStyle is always ready before the child is
processed. If an inheritable property (like color or
font-size) is not explicitly defined on the child element,
the StyleBuilder simply copies the resolved value from the
parent's ComputedStyle.
Style Invalidation Optimization
To maintain high frame rates, Chromium avoids recalculating styles
for the entire DOM tree when minor changes occur. When the DOM or CSSOM
is mutated—such as a JavaScript function toggling a class on a
div—Chromium uses "Invalidation Sets."
These sets allow the engine to quickly determine the exact
descendants or siblings that might be affected by that specific class
change. Chromium marks only those affected nodes as "dirty." When the
style recalculation phase actually runs, the DOM traversal skips clean
nodes entirely and only executes the bucketing, matching, and cascade
processes on the dirty nodes, updating their ComputedStyle
objects before passing the updated tree to the Layout phase.