The CSSOM
Date: 2026-08-17
The CSS Object Model (CSSOM) is the parsed tree of style rules, built alongside the DOM and required before anything can be laid out. It’s the reason a stylesheet blocks rendering while a script can be deferred — the browser cannot paint a single pixel without knowing what everything looks like.
Why it blocks
HTML ──parse──→ DOM ┐
├──→ RENDER TREE ──→ layout ──→ paint
CSS ──parse──→ CSSOM ┘
both are required. neither is optional.
The browser will not paint with a partial CSSOM, and the reason is that CSS cascades: a rule at the bottom of the last stylesheet can override everything above it. Painting early would mean painting wrong and repainting — a flash of unstyled content — so browsers block instead.
This is the asymmetry that surprises people:
<script src="app.js" defer> ← parser continues. render continues.
<link rel="stylesheet" href="…"> ← parser continues, RENDERING BLOCKS
There is no defer for a stylesheet. The nearest equivalents are loading it non-blocking and applying it on load, or media attributes that scope it out of the blocking set:
<!-- only blocks rendering when printing — i.e. never, for screen users -->
<link rel="stylesheet" href="/print.css" media="print">
<!-- the non-blocking trick: load as print, promote on arrival -->
<link rel="stylesheet" href="/below-fold.css" media="print"
onload="this.media='all'">How it differs from the DOM
| DOM | CSSOM | |
|---|---|---|
| Built from | HTML | All stylesheets, inline styles, <style> blocks |
| Construction | Incremental — usable while parsing | All-or-nothing — needs every rule before it’s valid |
| Blocks parsing | Only synchronous scripts do | No — parsing continues |
| Blocks rendering | Yes, it’s required | Yes |
| Matched | — | Right-to-left, per element |
Selectors are matched right to left, which is worth knowing because it inverts the intuition about what’s expensive:
.product-grid .card .title span
↑ the browser starts HERE
for every <span> on the page:
is its parent a .title? no → discard, next span
...
a short, specific rightmost selector is fast.
a broad one — span, *, :not(...) — makes the browser test far more elements
In practice selector performance is rarely the bottleneck on a real page — the volume of rules and the amount of layout they trigger matters far more — The Rendering Pipeline.
Reading it from JavaScript
The CSSOM is queryable, and one method on it is the single most common cause of self-inflicted jank.
// the RULES, as parsed
document.styleSheets[0].cssRules;
// the COMPUTED value — every property resolved for this element
const styles = getComputedStyle(el);
styles.getPropertyValue('padding-left'); // "16px" — always resolved,
// never "1rem"getComputedStyle forces layout if anything is pending. Reading a computed geometric value mid-loop causes a synchronous reflow per iteration:
// ✗ forced synchronous layout — read, write, read, write…
items.forEach(el => {
el.style.height = el.offsetHeight * 2 + 'px'; // read forces layout,
}); // write invalidates it
// ✓ batch: read everything, then write everything
const heights = items.map(el => el.offsetHeight); // all reads
items.forEach((el, i) => el.style.height = heights[i] * 2 + 'px');This is layout thrashing, and it’s the highest-value thing to recognise in this note — Reflow and Repaint.
Custom properties are a CSSOM feature
CSS custom properties (--brand-colour) live in the CSSOM and are inherited and resolved at runtime, which is what separates them from build-time variables in Sass or similar:
el.style.setProperty('--accent', '#c0392b'); // cascades to descendants,
// no re-render of anythingChanging one custom property on a root element restyles everything inheriting it — which is why theming via custom properties is cheap and theming via class swaps across a tree isn’t — Theming, Design Tokens.
Practical consequences
- Inline critical CSS in the head for above-the-fold content, and load the rest non-blocking. The first paint then needs no network round trip for styles — The Critical Rendering Path, Render-Blocking Resources
- Fewer, larger stylesheets beat many small ones for blocking purposes — every one is a request that must complete before rendering
@importinside CSS is a serial round trip. The browser can’t discover the imported file until the importing one has downloaded and parsed. Avoid it in production- Media queries don’t remove the download, only the render-blocking. A
media="print"stylesheet is still fetched, at low priority - Batch reads and writes, always, when touching layout-affecting properties from JavaScript
Where it interacts
- The DOM — the other half of the render tree
- The Rendering Pipeline — where the CSSOM feeds in, and what happens after
- Render-Blocking Resources — the practical treatment of the blocking problem this note explains
- Reflow and Repaint — what a style change costs once the CSSOM is built