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.
HTML → DOM ─┐
├→ Style → Layout → Paint → Compositing → Pixels
CSS → CSSOM ┘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.
<link rel="stylesheet" href="/app.css" />
<script>
console.log(getComputedStyle(document.body).color)
</script>| Script | Execution | Ordering |
|---|---|---|
| Classic | Blocks parser | Document order |
defer | After parsing | Document order |
async | As soon as ready | Load order |
type="module" | Deferred by default | Module dependency order |
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
@importcreates 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 useasync. - Failure: treating
deferas 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.
for (const card of cards) {
card.style.width = "50%"
console.log(card.offsetWidth)
}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: noneremoves a subtree from layout;visibility: hiddenpreserves 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
transformandopacitycan often run on the compositor without new layout or paint. Animatingwidth,height,top, orleftcommonly triggers layout and paint. - Failure: interleaving writes and reads — set
style.width, then readoffsetWidth— forces synchronous layout in a loop. Batch reads before writes.getBoundingClientRectis not inherently slow; it is expensive when it flushes pending work.will-changeeverywhere 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.
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
requestAnimationFrameto 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.
<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 withfont-display: swap. - On long pages,
content-visibility: autocan skip offscreen rendering work. CSS containment and list virtualization change behavior — introduce them after measurement. - Failure: overusing
preconnect/preload.preloadcompetes 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.
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.
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.