From harness-claude
Profiles and optimizes CSS style recalculation costs with Chrome DevTools. Covers selector matching, invalidation scopes, and techniques to keep under 4ms per frame amid class changes or complex rules.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Understand CSS selector matching, style invalidation, and recalculation costs — how browsers resolve computed styles for every visible element, why some selectors are orders of magnitude more expensive than others, and how to keep style recalculation under 4ms per frame.
Optimizes CSS performance using content-visibility, containment, efficient selectors, and Core Web Vitals patterns for long pages, CLS issues, large bundles, and animation jank.
Optimizes website and web app performance by measuring Core Web Vitals with Lighthouse, analyzing bundle sizes and bottlenecks, and implementing caching, code splitting, and asset optimizations.
Share bugs, ideas, or general feedback.
Understand CSS selector matching, style invalidation, and recalculation costs — how browsers resolve computed styles for every visible element, why some selectors are orders of magnitude more expensive than others, and how to keep style recalculation under 4ms per frame.
<body> or <html> causes a full-page style invalidationcontain: style) could isolate style recalculation to subtrees:nth-child() or * in compound selectorsProfile style recalculation cost. Open Chrome DevTools Performance panel. Record an interaction. Click on any purple "Recalculate Style" bar. The summary shows the number of elements affected and the time spent. Target: under 4ms for 60fps (you have ~10ms of JS budget per frame; style recalculation should consume less than half).
Understand right-to-left selector matching. Browsers evaluate selectors from right to left. For .sidebar .nav ul li a, the browser first finds all <a> elements (the key selector), then checks if each has an <li> parent, then <ul>, then .nav, then .sidebar. The key selector (rightmost) determines initial candidate set size.
/* SLOW — key selector `a` matches every link on the page,
then walks up 4 ancestors for each */
.sidebar .nav ul li a {
color: blue;
}
/* FAST — key selector `.sidebar-link` matches only targeted elements,
no ancestor walking needed */
.sidebar-link {
color: blue;
}
Reduce selector complexity. Aim for flat, single-class selectors. Each additional combinator (descendant, child, sibling) adds ancestor-walking cost:
/* O(elements * ancestors) — expensive */
.feed .card .content .text p {
margin: 0;
}
/* O(elements) — direct match, no tree walking */
.feed-text {
margin: 0;
}
Minimize style invalidation scope. When you add or remove a class, the browser must determine which elements' computed styles might change. Adding a class to <body> invalidates styles for the entire document. Adding a class to a leaf element invalidates only that element.
// BAD — invalidates styles for every element in the document
document.body.classList.toggle('dark-mode');
// BETTER — invalidates only the subtree
document.querySelector('.app-container').classList.toggle('dark-mode');
Use CSS Containment to create style boundaries. contain: style (part of contain: strict or contain: content) prevents style changes inside a container from triggering recalculation outside it.
.widget {
contain: layout style; /* Style changes inside do not affect siblings or ancestors */
}
Adopt atomic CSS for maximum selector performance. Atomic CSS (Tailwind, Stylex, Linaria extracted) maps each class to exactly one CSS property. Selector matching becomes O(1) per property because each class is unique and directly mapped.
<!-- Atomic: each class = one property, O(1) matching per class -->
<div class="flex items-center gap-4 p-2 bg-white rounded-lg shadow-sm">
<!-- Semantic: selector .card-header matches, then cascade resolves 8 properties -->
<div class="card-header"></div>
</div>
Style recalculation cost is approximately: O(elements_affected * selectors_evaluated). For a page with 2,000 elements and 3,000 selectors, a full recalculation evaluates up to 6 million selector-element pairs. Browsers optimize with Bloom filters (Blink) and rule hash tables (WebKit) that skip obviously non-matching selectors, but complex selectors defeat these optimizations.
Selector types ranked by matching cost (fastest to slowest):
#header) — hash lookup, O(1).nav-item) — hash lookup, O(1)div) — hash lookup, O(1)*) — matches everything, no filtering[data-active]) — linear scan in some engines:nth-child(3)) — requires counting siblings.a .b .c) — requires ancestor tree walkingLinkedIn's feed page had 50ms "Recalculate Style" events when loading new posts. The root cause: selectors like .feed-container .feed-card .card-content .text-body p required walking 4 ancestor levels for every <p> element. With 200 feed cards and 800 paragraphs, each style invalidation evaluated 800 * 4 ancestor levels. The fix: flattening to .feed-text-body reduced recalculation to 8ms — a 6x improvement — because the single-class selector matched directly without tree walking.
Facebook uses Stylex, their atomic CSS-in-JS framework, where each class maps to exactly one CSS declaration. A button with color: blue; padding: 8px; border-radius: 4px gets three atomic classes. Selector matching becomes O(1) per property because each class is a direct hash lookup with no combinators. On pages with 50,000+ elements, this architecture eliminated style recalculation as a performance bottleneck — recalculation times stayed under 2ms regardless of page complexity.
When a DOM mutation occurs (class change, attribute change, element insertion), the browser must determine which elements need their styles recalculated. Modern browsers use "style invalidation sets" — precomputed data structures that map mutations to affected selectors.
Types of invalidation:
:nth-child, ~, + selectors)<body> with descendant selectors)Universal selectors in compound selectors. *.active or div * forces the engine to evaluate against every element, defeating Bloom filter optimizations. The universal selector produces the largest possible initial candidate set.
Deeply nested descendant selectors. .a .b .c .d .e triggers expensive tree walks for each candidate element. Each descendant combinator requires walking up ancestors until a match is found or the root is reached. With a DOM depth of 30, each evaluation walks up to 30 ancestors per combinator.
:nth-child on large sibling lists. :nth-child(2n+1) requires counting siblings for each candidate element. On a list with 500 items, each evaluation counts up to 500 siblings. Sibling invalidation is also expensive — inserting a new sibling invalidates :nth-child for all existing siblings.
Adding/removing classes on <body>. If any selector in the stylesheet uses .body-class .something, toggling a class on <body> forces descendant invalidation for every element in the document. Scope the class change to the smallest possible subtree.
Overusing getComputedStyle() in loops. window.getComputedStyle(el) forces style recalculation if styles are dirty. Calling it inside a loop that also modifies styles creates a read-write-read-write pattern that triggers recalculation on every iteration:
// BAD — forces style recalculation on every iteration
elements.forEach((el) => {
el.style.width = '100px';
const height = getComputedStyle(el).height; // forces recalc
});
// GOOD — batch reads, then batch writes
const heights = elements.map((el) => getComputedStyle(el).height);
elements.forEach((el, i) => {
el.style.width = '100px';
});
CSS-in-JS runtime style injection. Frameworks that inject <style> tags at runtime force the browser to reparse the stylesheet and recompute styles for the entire document. This is especially expensive in hot paths like list rendering. Prefer static extraction (Linaria, vanilla-extract) or atomic CSS approaches.