Tags: web-dev concept

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

DOMCSSOM
Built fromHTMLAll stylesheets, inline styles, <style> blocks
ConstructionIncremental — usable while parsingAll-or-nothing — needs every rule before it’s valid
Blocks parsingOnly synchronous scripts doNo — parsing continues
Blocks renderingYes, it’s requiredYes
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 anything

Changing 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
  • @import inside 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