JavaScriptCSSDOMRendering

DOM and CSSOM: How Browsers Build and Render a Page

Understand DOM, CSSOM, render tree, layout, paint, and performance basics such as batching DOM updates, avoiding forced synchronous layout, and using requestAnimationFrame.

·Updated ·11 min read·Counting...
DOM and CSSOM: How Browsers Build and Render a Page

DOM and CSSOM are two core structures the browser builds before rendering a page. The DOM represents document content and structure. The CSSOM represents parsed CSS rules. Together, they help the browser build the render tree, calculate layout, paint pixels, and display the page.

Understanding them makes performance advice such as "batch DOM updates" and "avoid forced layout" more concrete.

What the DOM is

The Document Object Model represents HTML as a tree of nodes:

  • element nodes such as div, p, and a
  • text nodes
  • attributes
  • parent-child relationships

JavaScript can read and modify that tree:

const element = document.getElementById("demo")
element.textContent = "Hello, World!"

const paragraph = document.createElement("p")
paragraph.textContent = "A new paragraph"
document.body.appendChild(paragraph)

Common DOM APIs include:

  • getElementById
  • querySelector
  • querySelectorAll
  • createElement
  • appendChild
  • classList
  • addEventListener

What the CSSOM is

The CSS Object Model represents parsed CSS rules and computed style information. The browser parses CSS files, style tags, and inline style information into structures it can use for rendering.

JavaScript can affect CSSOM-related behavior:

document.getElementById("demo").style.color = "red"
document.getElementById("demo").classList.add("is-active")

Prefer class changes for repeated visual states. Direct inline style edits are fine for dynamic values, but they can become hard to maintain when used everywhere.

Rendering in simplified steps

The browser roughly does this:

  1. Parse HTML into the DOM.
  2. Parse CSS into the CSSOM.
  3. Combine visible DOM nodes and style information into a render tree.
  4. Calculate layout: positions and sizes.
  5. Paint pixels.
  6. Composite layers and display.

The real browser pipeline is more complex, but this model is enough to reason about many performance problems.

For animation-specific rendering costs, read Reflow and Repaint: Why CSS Animations Get Janky.

Batch DOM updates

Many small DOM writes can be expensive. Build a fragment and insert once:

const fragment = document.createDocumentFragment()

for (let index = 0; index < 1000; index += 1) {
  const item = document.createElement("li")
  item.textContent = `Item ${index}`
  fragment.appendChild(item)
}

document.querySelector("ul").appendChild(fragment)

Modern frameworks batch much of this for you, but direct DOM code still benefits from this pattern.

Avoid forced synchronous layout

Reading layout values after writing layout-affecting styles can force the browser to calculate layout immediately:

element.style.width = "200px"
const height = element.offsetHeight

When possible, group reads before writes:

const height = element.offsetHeight
element.style.width = "200px"

This is especially important in scroll, resize, drag, and animation code.

Use class changes for visual state

Instead of repeatedly setting many inline styles, define CSS and toggle classes:

.positioned {
  position: absolute;
}
element.classList.add("positioned")

This keeps style rules centralized and easier to inspect.

Use requestAnimationFrame for visual updates

function animate() {
  element.style.transform = `translateX(${x}px)`
  requestAnimationFrame(animate)
}

requestAnimationFrame(animate)

requestAnimationFrame schedules work with the browser's rendering cadence. It does not automatically make heavy work cheap, but it is a better fit for visual updates than arbitrary timers.

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.