CSS has a reputation for being the cheap part of a page. Measured in bytes, that holds: the median mobile home page ships 77 KB (Web Almanac 2025) of stylesheets against 632 KB (Web Almanac 2025) of JavaScript. Measured in main-thread time, it stops holding. Every class change, every disclosure toggle, every state switch forces the browser to work out the computed styles for the affected nodes again – and that step sits inside the same deadline as the response to the click. A good INP result requires 75 percent (Web Almanac 2025) of sessions to land at 200 milliseconds (Web Almanac 2025) or below; style calculation sits inside that budget. This article shows what the cost is made of, how to make it visible in the lab and in the field, and which changes actually shrink the scope of the recalculation. Where the time goes on a specific site is what a technical performance analysis answers.
Key takeaways
- Style calculation has two cost drivers: finding the matching rules and assembling the result. Roughly half of the time in Chromium's rendering engine goes into matching selectors (web.dev).
- The upper bound of the cost is the number of elements multiplied by the number of selectors (web.dev). Both factors grow independently of each other – and both can be reduced separately.
- The corpus gives the order of magnitude: at the median a mobile page carries 613 (Web Almanac 2022) style rules, at the 90th percentile 2,023 (Web Almanac 2022). That load is spread across 8 (Web Almanac 2025) CSS files at the median.
- In the lab, the Selector Stats tab exposes the expensive rules: alongside elapsed time it shows match attempts and actual matches – a selector with 1,104 (Chrome for Developers) attempts and not a single match is pure wasted work.
- In the field, the Long Animation Frames interface reports
styleAndLayoutStart, the beginning of the style and layout period inside a frame (Chrome for Developers). That turns the share into evidence instead of a guess. - Scope follows node count: the DOM audit warns at about 800 (Lighthouse) nodes in the body and reports an error at about 1,400 (Lighthouse) nodes.
What the browser does before anything becomes visible
Between a state change in script and the next frame on screen sit four pieces of work: style calculation, layout, paint and compositing. The first of them answers a deceptively simple question – which declarations apply to this element? To answer it, the engine first builds the set of matching selectors and then processes their rules into a computed style. The split of that work is documented: roughly half of the time used in Chromium's rendering engine to calculate the computed style for an element is used to match selectors, the other half to construct the computed style representation from the matched rules (web.dev). Tuning selectors alone therefore covers at most half of the bill.
The second half of the bill is scope. The documentation states the upper bound plainly: in the worst case the cost of calculating computed styles is the number of elements multiplied by the selector count, because the engine has to check each element at least once against every rule (web.dev). Modern engines undercut that bound with per-element invalidation sets, but the direction stays the same: the higher up the tree a class is toggled, the more descendants enter the check. A class on body is a different operation from the same class on a card inside a list, even though both changes look identical in the editor. Why a large node count creates cost in several places at once is covered in the article on reducing DOM size.
Why the bill is harsher on mobile
Two kinds of cost: match attempts and scope
The first kind of cost is easy to measure and rarely measured: match attempts without a match. The engine checks a selector against an element, finds that it does not apply and discards the result. That work shows up in no coverage report, because the rule is correctly unused – it still costs time. The example output in the Chrome documentation contains one sentence that makes the order of magnitude tangible: the engine attempted to match this single selector 1,104 (Chrome for Developers) times without matching any element. Rules like that typically come from page builders, campaign templates and old theme packages that travel along in every bundle. How to clear that corpus systematically is described in the article on finding and removing unused CSS.
The second kind of cost is invalidation scope. It does not originate in the stylesheet but in the script: at the point where a class is set, an attribute is changed or a property is written. The further up the node sits and the wider the affected selectors reach, the more elements move into re-evaluation. In the documentation's example recording the report shows, alongside the duration, the number of affected elements – just over 900 (web.dev) in that case, for a style calculation of just over 25 milliseconds (web.dev). That is not an extreme case but an ordinary toggle; those 25 milliseconds already consume an eighth of the deadline available for the whole interaction.
| Kind of cost | How to spot it | Where it becomes visible | Sourced order of magnitude |
|---|---|---|---|
| Match attempts without matches | high attempt count, match count near zero | Selector Stats in the recording | 1,104 attempts, 0 matches (Chrome for Developers) |
| Rule count per page | many bundles, many theme packages | number of style rules per document | 613 at the median, 2,023 at the 90th percentile (Web Almanac 2022) |
| File split | many separate stylesheets | network view of the recording | 8 files at the median, 31 at the 90th percentile (Web Almanac 2025) |
| Invalidation scope | toggle high up in the tree | number of affected elements in the event | around 900 elements at just over 25 ms (web.dev) |
| Node count of the document | long lists, deep nesting | DOM audit in the report | warning at about 800, error at about 1,400 nodes (Lighthouse) |
The four rows belong together, but they are found with different tools. Rule count and file split live in the bundle, match attempts live in the recording, invalidation scope lives in the script. A report that only states the byte size of the CSS covers exactly one of those four rows – and precisely the one that hurts least on fast connections. What a check looks like that pulls all four levels together is shown by our front-end optimization.
How much CSS is actually shipped today
Without corpus figures every recommendation is a matter of taste, so a short look at the distribution. For the median mobile home page 77 KB (Web Almanac 2025) of stylesheets are transferred, for inner pages 80 KB (Web Almanac 2025). That volume is spread over 8 (Web Almanac 2025) CSS files at the median; at the 90th percentile it is 31 (Web Almanac 2025) files. The rule count behind it was last surveyed systematically in 2022: 613 (Web Almanac 2022) style rules per mobile page at the median, 1,197 (Web Almanac 2022) at the 75th and 2,023 (Web Almanac 2022) at the 90th percentile. Those rules are the right-hand side of the multiplication from the documentation – and they rarely shrink on their own.
The distribution is skewed
For transferred stylesheet sizes, the mobile percentiles in 2022 were 6, 28, 68, 139 and 256 KB (Web Almanac 2022). Between the median and the 90th percentile that is almost a factor of four. An average describes such a distribution poorly; what matters is where your own site sits.
The universal selector is everyday practice
Almost half of all pages analysed apply border-box sizing to every element via the universal selector (Web Almanac 2022). As a pattern that is widespread and usually harmless, but it shows how naturally rules get written that touch the entire tree.
The deadline is fixed
For a good INP result, 75 percent (Web Almanac 2025) of sessions have to be at 200 milliseconds (Web Almanac 2025) or below. Input delay, processing and presentation all sit inside that window – style calculation shares the budget with everything else.
What is interesting is where that pressure surfaces first. On mobile, home pages reach a good INP in 80 percent (Web Almanac 2025) of cases, secondary pages in only 69 percent (Web Almanac 2025). That is exactly where the interactive surfaces live: filters, variants, quantity fields, disclosure panels. Anyone checking the home page and skipping the category page is measuring the friendlier part of the corpus. What that means in the purchase flow, where every toggle is immediately visible, is covered in the article on checkout speed in the final seconds before the order.
The core in one sentence
In the lab: recalculate style and Selector Stats
In a recording of the performance panel the entry to look for is called Recalculate Style. It appears where the browser works out the computed styles for part of the tree again, and it carries two figures that together support the diagnosis: the duration and the number of affected elements. When those entries get long, the Selector Stats tab helps to understand which of your CSS selectors takes the most time (Chrome for Developers). The table lists elapsed time, match attempts and actual matches per selector – which separates wasted work from necessary work instead of trimming by intuition.
One detail decides whether the measurement is usable: Selector Stats is turned off by default because it adds more overhead to performance recordings (Chrome for Developers). The absolute values of a recording with the statistic enabled are therefore not comparable with those of an ordinary recording. For diagnosis that is irrelevant, because the ranking of selectors is preserved; for a before-and-after proof it is decisive, because otherwise the measurement setup itself creates the difference. Two recordings, same setting, same sequence – otherwise you are comparing two different measurements.
// Rough corpus figure before the recording: how many rules are in the
// document at all, and how do they spread across sheets?
const sheets = [...document.styleSheets];
let rules = 0, unreadable = 0;
for (const sheet of sheets) {
try {
rules += sheet.cssRules.length;
} catch (e) {
// Cross-origin without CORS clearance: not readable, do not guess
unreadable += 1;
}
}
console.table({
stylesheets: sheets.length,
rules,
unreadable,
nodesInBody: document.body.getElementsByTagName('*').length
});An unreadable stylesheet is not an empty stylesheet
cssRules fails for cross-origin stylesheets without the corresponding clearance. Anyone catching the error and silently counting zero rules ends up with a corpus figure that is too small – short by exactly those bundles that do not come from their own build. That is why the example counts the unreadable sheets separately instead of booking them as zero. A number without a statement about what it excludes is usually the more expensive option.In the field: the frame as a timeline
A lab recording shows one sequence on one device. Whether the same toggle is expensive for visitors is a question it does not answer. That is what the Long Animation Frames interface is for, which splits a frame into timestamps: startTime marks the beginning, renderStart the start of rendering work and styleAndLayoutStart the beginning of the time period spent in style and layout calculations (Chrome for Developers). The difference between the end of the frame and that timestamp gives the duration of the style and layout phase – in the documentation's worked example that is 111 milliseconds (web.dev) inside a single frame.
In addition, every recorded script entry carries the field forcedStyleAndLayoutDuration, which reports the time spent on forced style and layout work inside that function (Chrome for Developers). That is the classic case where an event handler writes and immediately afterwards reads a geometry, forcing the engine into an instant recalculation. The value names the culprit instead of only reporting the total. How to turn those frames into a defensible priority order is described in the article on Long Animation Frames as an INP bottleneck; the metric itself is put into context in the article on optimizing INP.
// Determine the style and layout share per long frame.
// Only timing values from your own document are collected.
const observer = new PerformanceObserver((list) => {
for (const frame of list.getEntries()) {
const end = frame.startTime + frame.duration;
const styleLayout = frame.styleAndLayoutStart
? end - frame.styleAndLayoutStart
: null; // not measured, do not set to zero
const forced = frame.scripts.reduce(
(total, s) => total + (s.forcedStyleAndLayoutDuration || 0), 0);
report({
frameDuration: Math.round(frame.duration),
styleAndLayout: styleLayout === null ? null : Math.round(styleLayout),
forced: Math.round(forced),
trigger: frame.scripts[0]?.invoker ?? 'unknown'
});
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });What really shrinks the scope
The obvious answer is: shorter selectors. It is correct, but it is the smaller half. The documentation explicitly names the number of re-evaluated elements as the often more important lever compared with selector complexity (web.dev). In practice that means: a toggle that happens on the container actually affected by the change, rather than on the root element, shrinks the check area immediately – without rewriting a single rule. And it is usually implemented faster than a rebuild of the stylesheet.
- Put the toggle point as deep in the tree as possible. A class on
bodypotentially pulls the entire subtree into re-evaluation; the same class on the affected section stays local. That is a one-line script change, not a stylesheet migration. - Express states through a fixed set of classes instead of inline styles written in a loop. Writing and reading inside a loop forces recalculation in the middle of the event handler – which is exactly what
forcedStyleAndLayoutDurationreports (Chrome for Developers). - Reduce deeply nested selectors and position-dependent pseudo-classes to a class on the target element. The documentation explains this by noting that the engine has to know the sibling situation first for position-dependent checks (web.dev).
- Keep an eye on node count. The DOM audit warns at about 800 (Lighthouse) nodes in the body and reports an error at about 1,400 (Lighthouse) nodes; long lists without offloading are the most common reason for crossing those marks.
- Exclude areas outside the viewport from rendering work instead of computing them along the way. How that works with built-in means is described in the article on content-visibility and skipped rendering work.
- Trim the rule count before new bundles arrive. With 8 (Web Almanac 2025) CSS files at the median and 31 (Web Almanac 2025) at the 90th percentile, the question is rarely whether something is missing, but which package has been riding along without a job since the last rebuild.
- Separate the critical part from the rest so the first render does not wait for the whole rule set. The approach is described in the article on critical CSS above the fold.
Two points belong together: animations and style calculation. A transition that changes a property touching geometry triggers style and layout work again in every frame. The timeline shows that as a chain of similar entries across many frames. Which properties can be animated without that chain is covered in the article on smooth animations without jank. For shops there is an added twist: variant and filter toggles frequently force whole lists into re-evaluation – a pattern that shapes Shopware performance more strongly than the home page does.
In general terms, the worst case cost of calculating the computed elements style is the number of elements multiplied by the selector count, because the browser needs to check each element at least once against every style to see if it matches.
Proof needs a second look. A lab recording shows that a selector consumes time; only field data shows that this time reaches visitors. Putting both sources side by side is a work step of its own, described in the article on field data and lab values. And because collection in the field touches questions about personal data, the legal side belongs in the same plan: what is permissible without consent is covered in the article on performance measurement without consent. How the effort spreads across the three metrics overall is set out in our overview of the Core Web Vitals.
Sources and Studies
Recalculate Style entries have to consume visible time, and in the Selector Stats tab that time has to concentrate on a few selectors (Chrome for Developers). In the field, the Long Animation Frames interface supplies styleAndLayoutStart, the beginning of the style and layout phase (Chrome for Developers). If the share is low in both sources, effort typically pays off better elsewhere.Related Articles
Reduce DOM Size: Fewer Nodes, Faster Interactions
Reduce DOM size: cut style and layout work with pagination, virtualization, content-visibility and flatter markup to improve INP and Core Web Vitals.
Finding and Removing Unused CSS for Faster Loads
How much CSS a page really needs, how to measure the unused share reliably and in which order to remove ballast without breaking the rendering.
Frontend Memory Leaks: When Long Sessions Slow Down
The interface starts fast and turns sluggish after twenty minutes: how front-end memory leaks arise, how to measure them over time and how to get rid of them.