Skip to content

A browser does not wait for a complete page and render it once. It streams HTML, discovers resources, builds internal trees, runs JavaScript, calculates geometry, and paints incrementally.

The critical rendering path is the dependency chain between navigation and the pixels needed for the initial view. The useful question is: which resource or main-thread task is keeping the browser from producing the next important frame?



1. The Rendering Pipeline

The simplified pipeline is HTML → DOM, CSS → CSSOM, then style, layout, paint, compositing, pixels. Real browsers pipeline and repeat these stages.


text
HTML → DOM  ─┐
             ├→ Style → Layout → Paint → Compositing → Pixels
CSS  → CSSOM ┘

text
Navigation → HTML bytes arrive
  ├─ HTML parser builds DOM
  │    ├─ CSSOM ready?
  │    │    No  → Cannot paint safely
  │    │    Yes → Style → Layout → Paint
  │    └─ Parser-blocking script?
  │         Yes → Pause parsing → Download + execute JS → back to parser
  │         No  → Continue parsing
  └─ Preload scanner discovers resources → Download CSS / JS / images
       └─ CSS download also feeds CSSOM ready

  • The DOM can build while HTML is still arriving. The browser can paint before the document is complete. JavaScript may later invalidate style, layout, or paint.
  • The path is a dependency graph, not a fixed sequence.
  • A late stylesheet delays rendering. A parser-blocking script delays markup and resource discovery. A large JavaScript task delays painting even when every resource is cached.

2. Network, HTML, and Resource Discovery

Rendering starts with the document request. DNS, connection setup, TLS, redirects, and server response time all happen before the browser can parse useful HTML. A slow Time to First Byte shifts every later stage.

  • Streaming HTML lets the browser process the response before the server finishes. Critical CSS, fonts, images, and visible markup should appear early.
  • The HTML parser builds the DOM incrementally. A speculative preload scanner looks ahead for <link rel="stylesheet">, <script src>, <img src> / srcset, and <link rel="preload">.
  • The scanner can start requests while the main parser is blocked. It cannot reliably discover resources created by JavaScript, returned by a later API, or referenced inside CSS that has not loaded.
  • Failure: an image URL computed after JavaScript and data fetching. An <img src> in HTML is immediately visible to the browser. Declarative HTML is a performance feature.

3. CSS and JavaScript Blocking

CSS is parsed into the CSSOM. The browser needs applicable styles before it can determine which boxes exist and how they should look. An external stylesheet in the document head is normally render-blocking.


html
<link rel="stylesheet" href="/app.css" />
<script>
  console.log(getComputedStyle(document.body).color)
</script>

ScriptExecutionOrdering
ClassicBlocks parserDocument order
deferAfter parsingDocument order
asyncAs soon as readyLoad order
type="module"Deferred by defaultModule dependency order

text
Classic script:
  Parser → Network: Discover /legacy.js
  Parser: Pause HTML parsing
  Network → Main thread: Script ready
  Main thread: Execute script
  Main thread → Parser: Resume parsing

defer / module:
  Parser → Network: Discover /app.js
  Parser: Keep parsing HTML
  Network → Main thread: Script ready
  Parser → Main thread: DOM complete
  Main thread: Execute deferred scripts

  • CSS usually does not stop DOM construction, but it prevents painting content that depends on incomplete styles. It can also delay a following script because JavaScript may inspect computed styles — the script waits for CSS, and the parser waits for the script.
  • CSS @import creates a waterfall: imported files are discovered only after the parent stylesheet is parsed. Direct <link> exposes requests earlier.
  • Application code should usually use a module or defer. Independent analytics may use async.
  • Failure: treating defer as free. It removes parser blocking, not CPU cost. Transfer, parse, compile, and execute still happen. A large startup task can delay rendering after the DOM is ready.

4. Style, Layout, Paint, and Compositing

Style resolves the cascade. Layout calculates geometry. Paint records drawing commands. Compositing assembles layers into frames.


ts
for (const card of cards) {
  card.style.width = "50%"
  console.log(card.offsetWidth)
}

text
Property change → Which stage?
  width / height / top / left     → Layout → Paint → Composite
  color / background / box-shadow → Paint → Composite
  transform / opacity             → Composite only

  • DOM nodes do not map one-to-one to boxes: display: none removes a subtree from layout; visibility: hidden preserves space; pseudo-elements can create boxes without DOM nodes.
  • Large DOMs and broad invalidation usually matter more than micro-optimizing ordinary selectors. A width change can rewrap text and move everything below a container.
  • Animating transform and opacity can often run on the compositor without new layout or paint. Animating width, height, top, or left commonly triggers layout and paint.
  • Failure: interleaving writes and reads — set style.width, then read offsetWidth — forces synchronous layout in a loop. Batch reads before writes. getBoundingClientRect is not inherently slow; it is expensive when it flushes pending work. will-change everywhere can make performance worse: layers use memory.

5. The Main Thread and Rendering Opportunities

JavaScript and most rendering work share the main thread. The browser does not paint after every task, but it needs the main thread to become available.


text
Run one task → Drain microtasks → Update rendering?
  Yes → requestAnimationFrame → Style → Layout → Paint → Composite → Next task
  No  → Next task

  • Long JavaScript tasks delay input and frames.
  • Use requestAnimationFrame to align visual writes with a frame, not as a place for expensive computation. Move suitable CPU work to a Web Worker when its communication cost is justified.
  • Failure: an unbounded microtask chain starves rendering because the checkpoint drains before the browser can update the rendering.

6. Optimizing the Initial View

The initial render needs visible HTML, applicable CSS, blocking scripts, main-thread rendering time, and any image or font required by the result. It does not need below-the-fold media, analytics, chat widgets, or most interaction code.


html
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link
  rel="preload"
  href="/hero.avif"
  as="image"
  type="image/avif"
  fetchpriority="high"
/>
<img
  src="/hero.avif"
  alt="Browser rendering pipeline"
  width="1600"
  height="900"
  fetchpriority="high"
/>

  • Reduce redirects and TTFB; stream useful HTML early. Make critical CSS, fonts, and the LCP image discoverable from HTML.
  • Remove parser-blocking scripts and CSS import chains. Reduce critical CSS and JavaScript, not only total page size. Keep third-party code outside the initial path.
  • Give images dimensions so layout space exists before they load. Do not lazy-load the LCP image. Use loading="lazy" for offscreen media. Serve WOFF2 subsets with font-display: swap.
  • On long pages, content-visibility: auto can skip offscreen rendering work. CSS containment and list virtualization change behavior — introduce them after measurement.
  • Failure: overusing preconnect / preload. preload competes for bandwidth and must match the final request's type and credentials mode.

7. Framework Rendering and Hydration

A client-rendered application often delays meaningful content until JavaScript, framework startup, and data fetching complete. Server rendering moves content and resource references into HTML, allowing earlier discovery and paint. It does not remove JavaScript cost.


text
Client render:
  HTML shell → JS → Data → Paint

Server render + hydrate:
  HTML with content → Early paint
  HTML with content → JS download → Hydration
  Early paint → Hydration

  • A page can paint quickly and then become unresponsive while a large hydration task runs.
  • Streaming SSR, selective hydration, islands, and Server Components all target the same problem: reduce startup work and keep non-critical JavaScript out of the initial dependency chain.
  • Ask which content must be in the first HTML, which components need client JS, when code and data become discoverable, whether hydration can happen in smaller boundaries, and whether the framework is creating server-to-client request waterfalls.
  • Failure: treating a fast first paint as a fast page. Hydration can still occupy the main thread afterward.

8. Measuring the Path

Use a production build. Inspect the Network waterfall and a Performance trace.


text
Navigation → TTFB → Resource load delay → Resource load duration → Element render delay → LCP

Resource load delay:      late discovery / low priority → Expose in HTML earlier
Resource load duration:   large asset / slow origin     → Compress / CDN / resize
Element render delay:     CSS / fonts / main thread     → Unblock style and paint

  • Network reveals late discovery, priority, queueing, and transfer. Main reveals parsing, script execution, style, layout, paint, and long tasks. Frames reveals missed deadlines. Experience reveals layout shifts and Core Web Vitals.
  • For slow Largest Contentful Paint, split time into TTFB, resource load delay, resource load duration, and element render delay. High load delay → discovery or priority. High load duration → asset size or origin. High render delay → CSS, fonts, decoding, JavaScript, or main-thread contention.
  • FCP: first content. LCP: largest visible content. CLS: unexpected movement. INP: interaction latency until the next paint.
  • Lab traces explain causes; real-user monitoring shows how often users encounter them.
  • Failure: a fast LCP with poor INP. Hydration or third-party scripts can occupy the main thread after the largest paint.

Final Mental Model

The critical rendering path consumes three limited resources: network time to discover and transfer critical bytes, main-thread time to parse, execute, style, and lay out, and rendering capacity to paint, rasterize, and composite frames.

Fast pages expose important resources early, avoid accidental blockers, minimize startup JavaScript, reserve layout space, and defer work outside the initial viewport. The goal is the shortest dependency chain between navigation and the pixels the user came to see.


Recap Q&A

Read the next note
React 19 New Features