Chromium Rendering Pipeline for Web Developers

Before the browser draws anything, it has already distributed work across at least three separate operating system processes. The Browser process is privileged: it orchestrates everything, manages tabs, and handles navigation. The Renderer process is sandboxed, typically one per tab, and it is where actual page content gets processed. The GPU process sits apart from both, receiving graphics commands and translating them for the platform's 3D API. It exists in isolation largely because GPU drivers have historically been a significant attack surface.
That sandboxing is not incidental. The Renderer process cannot touch system resources directly, which is precisely why the GPU process exists as a separate communication layer. They talk through IPC and a system called Mojo. Every handoff between processes introduces latency, and the first step toward not inadvertently multiplying that latency is simply knowing it exists.
Inside the Renderer process, work divides further across threads. The main thread handles parsing, styling, layout, and JavaScript. The compositor thread manages scrolling and animation independently of the main thread. Raster threads convert paint instructions into actual pixel tiles. These threads do not run in lockstep; they are designed to decouple, so the compositor can keep producing frames even when the main thread is occupied.
One thing worth knowing from painful experience: thread counts scale with hardware. A developer machine gets more compositor worker threads than a mid-range Android phone. Performance profiles recorded on a desktop can dramatically understate the cost a real user on constrained hardware will pay. The pipeline behaves differently on a budget device, and that difference will surprise you the first time you see it in production data.
The main thread's role and why its congestion is the root cause of most performance problems
The main thread does a punishing amount of work. HTML parsing, CSS parsing, JavaScript execution, style recalculation, layout, hit testing, and event dispatching all run there. Chromium's own documentation describes it as a thread that "routinely stalls for tens to hundreds of milliseconds," and that is not a bug report; it is an architectural acknowledgment that some of this cost is simply content-dependent.
A Long Task is formally defined as any task that occupies the main thread for more than 50 milliseconds. Long Tasks are the direct mechanism behind poor Interaction to Next Paint scores. When the main thread is busy, user input events queue, and they do not process until the main thread is free. That delay is what users register as sluggishness.
The compositor thread exists specifically to provide relief. It can scroll and animate from a snapshot of the page even while the main thread is completely blocked, but only if the developer has structured the work to permit it. If an animation requires the main thread to recalculate layout on every frame, the compositor's independence becomes irrelevant; the animation is now a dependent of main-thread output, and that dependency is the problem.
Every stage a developer can move off the main thread, or skip entirely, shrinks the window for Long Tasks. That is not a philosophy; it is a structural consequence of how the pipeline is built.
Stage 1 through Stage 4 — how the browser turns HTML and CSS into a positioned, decorated element tree
The pipeline begins before style, with animation. The Animate stage runs first, mutating computed styles and property trees on declarative timelines so that animated values are already resolved by the time Style begins. This ordering prevents style recalculation from operating on stale animated values, which is the kind of subtle correctness guarantee you only appreciate after debugging the alternative.
Style applies CSS rules to the DOM and produces computed styles. Every element gets one, and that output feeds directly into layout. Selector complexity matters here: a deeply nested selector or a broad universal rule forces the engine to evaluate more elements. Any forced style recalculation delays everything downstream because layout cannot begin until style has finished.
Layout takes computed styles and determines the exact size and position of every element, producing what Chromium calls the fragment tree. That tree is immutable once produced. A single DOM node can generate multiple fragments, across line breaks in inline content, for instance. When a property that affects geometry changes, some portion of the fragment tree is invalidated and layout must re-run for that subtree, or in the worst case, for the entire document. This is reflow, and its cost scales with how much of the tree is affected.
Pre-paint follows layout and gets less attention than it deserves. It computes four separate property trees covering transform, clip, effect, and scroll. These structures carry geometry information that can travel across threads without shipping the full DOM. They are also the foundation for Core Web Vitals measurement; layout shift and largest contentful paint are both derived from property tree data. Pre-paint additionally invalidates existing display lists and GPU texture tiles where content has changed, marking them for regeneration.
All four stages run on the main thread. Any work that forces them to re-run competes directly with JavaScript execution and event handling.
Stage 5 through Stage 7 — from scroll and paint to the commit that hands work to the compositor
Stage 5 is scroll. When a user scrolls, the engine updates scroll offsets by mutating the property trees computed in pre-paint. It does not require a full layout re-run because the fragment tree is unchanged; only the scroll offset within a property tree node updates. This is why compositor-driven scrolling feels instantaneous: it sidesteps the expensive upstream stages entirely and operates on lightweight, thread-safe data structures.
Stage 6 is paint. Paint does not produce pixels. It produces a display list, a recorded sequence of drawing instructions organized into paint chunks, describing how to rasterize GPU texture tiles later. Paint-only properties, things like background-color, color, and box-shadow, skip layout because the fragment tree is unchanged, but they still require the display list to be regenerated and tiles to be re-rasterized. That is cheaper than a full reflow, but it is not free, and conflating "no layout" with "no cost" is a mistake I have seen cause real regressions.
Stage 7 is commit. This is a blocking synchronization point where the property trees and display list are copied from the main thread to the compositor thread. The main thread pauses during this transfer. Once commit completes, the compositor has enough information to produce frames independently, and it will continue doing so even if the main thread subsequently gets blocked.
The most important developer leverage point in the entire pipeline lives here. If only compositor-driven properties change, specifically transform and opacity, the pipeline can skip Layout, Pre-paint, and Paint entirely. The animation runs on the compositor thread, insulated from whatever the main thread is doing. This is not a minor optimization; it is the difference between animations that hold 60 frames per second under load and animations that stutter whenever JavaScript runs.
How layerization, rasterization, and the GPU path convert display lists into pixels
After commit, the compositor thread decides which content gets its own composited layer, a GPU texture that can be updated independently. In RenderingNG, this layerization step happens after paint, not before. That is a deliberate departure from older architectures; paint records are the actual input to the layerization decision, making assignment more accurate than the old heuristic approaches.
Each composited layer is a texture in GPU memory. The compositor thread can reposition, scale, or change the opacity of a layer without touching the main thread at all. The cost of that independence is memory, and it is not a soft constraint. I have seen excessive layer promotion cause browser crashes because the device simply ran out of GPU memory. Promotion is a trade-off, not a free optimization, and it should be treated accordingly.
Rasterization converts display list paint records into pixel tiles using Skia as the graphics library, GPU-accelerated by default. Tiles are prioritized by proximity to the viewport, whether the layer is animating, and predicted scroll velocity. Tiles far from the viewport are rasterized at lower priority or lower resolution. When a user scrolls into tiles that have not yet been rasterized, Chrome can serve a low-resolution placeholder rather than showing a blank region; this is checkerboarding mitigation.
The final step, draw and display, is handled by the Viz process, the display compositor. It aggregates compositor frames from each renderer process and the browser process into a single frame for presentation, using Direct3D on Windows and OpenGL elsewhere. The display compositor runs on its own thread to remain responsive regardless of GPU driver latency.
The three pathways through the pipeline and what CSS properties determine which one fires
Not every CSS change costs the same. The pipeline has three distinct pathways, and the property being changed determines which one fires.
The first and most expensive pathway is Layout, then Paint, then Composite. Any property that changes element geometry triggers it: width, height, top, left, margin, padding, font-size. The browser must reflow the document, regenerate paint records for affected regions, and composite. At 60 Hz, the total frame budget is roughly 16.5 milliseconds; with browser overhead, developer code gets approximately 10 of those. A geometry-triggering animation eats into that budget on every single frame.
The second pathway is Paint, then Composite. Paint-only properties, background-color, color, box-shadow, background-image, skip layout because the fragment tree is unchanged. The display list still has to be regenerated and tiles re-rasterized. Cheaper, but not free.
The third pathway is composite only. Transform and opacity changes on promoted elements skip layout, pre-paint, and paint entirely and run on the compositor thread. This is the only reliable way to animate within the 10-millisecond budget under real load.
The practical mapping is direct: animate position with transform rather than top or left; animate transparency with opacity rather than visibility or display. Web.dev maintains a CSS Triggers reference, updated December 2023, that catalogs which properties fire which pathway. Worth bookmarking.
Forced reflow — how reading layout metrics from JavaScript breaks the pipeline's ordering
The pipeline's normal operation separates JavaScript execution from style and layout: JavaScript runs, then style recalculates, then layout runs. Forced reflow collapses that separation by demanding layout answers synchronously, inside the JavaScript task, before the script can continue.
Any call that reads a layout metric forces this: offsetLeft, offsetTop, offsetWidth, offsetHeight, getBoundingClientRect(), getClientRects(). The browser cannot return a correct value unless layout is current, so it immediately completes style recalculation and layout before returning. If a layout property was also written earlier in the same script block, the previously computed layout is now invalid and must be discarded. The browser recomputes, the script reads, the script writes again, the cycle repeats. This is layout thrashing. It is one of the most common performance problems in production JavaScript, and it shows up constantly in code that looks perfectly reasonable at first glance.
Chrome's guidance is that no forced reflow should exceed 30 milliseconds; violations above this threshold are flagged as actionable insights in the DevTools Performance Insights panel, per developer.chrome.com documentation updated October 2025. The console will also surface violation messages directly: "Forced reflow while executing JavaScript took 47ms" is the documented example form.
The impact on Core Web Vitals is measurable. Forced synchronous layout delays Largest Contentful Paint because the browser cannot paint the largest element until layout has completed. It also adds directly to Interaction to Next Paint latency; the INP "good" threshold is 200 milliseconds, and a single badly placed layout read can consume a substantial portion of that budget before the interaction response has even begun.
The fix is straightforward in principle and requires discipline in practice: batch all DOM reads before any DOM writes within a frame. Use requestAnimationFrame to defer write operations to the beginning of the next frame, where layout state is already settled from the previous frame's pipeline run.
Layer promotion with will-change and transform — what it actually does and where it misleads
will-change is a CSS declaration that signals to the browser that a specified property is expected to change, prompting layer promotion in advance rather than reactively when the animation begins. That is legitimate and useful. It also has a narrow set of conditions under which it actually helps.
What will-change does not do is skip pipeline stages. If a layout-triggering property later changes on a promoted element, layout still runs on the main thread before the layer is repainted. Promotion only makes the final compositing step cheaper. A developer who applies will-change to an element and then animates its width has paid GPU memory for a layer and received nothing in return, because the expensive part of the pipeline still ran.
The legacy form of this pattern, transform: translateZ(0), forces layer promotion by creating a 3D transform context. Per Chromium documentation, if the animation on a promoted element also changes a layout property, the promotion adds GPU memory overhead without eliminating the main-thread layout cost. It is a technique from an older era of the engine and should be treated as such.
The genuine benefit from promotion is narrow: elements whose only changes will be transform or opacity. Those properties can be animated entirely on the compositor thread. That is what you are actually paying the GPU memory for.
And that memory cost is real. Each layer is a GPU texture. Over-promoting across a large or complex page can exhaust device memory, and this has been a documented cause of browser crashes. The practical rule: apply will-change to specific elements that will definitely animate via transform or opacity, remove it after the animation completes if the element is not persistently animated, and never apply it broadly as a preemptive optimization. Broad application is not performance engineering; it is GPU memory consumption that happens to look like one.
Reading the pipeline in Chrome DevTools — how to locate which stage is costing time
The Performance panel is where abstraction meets evidence. Its flame chart maps directly onto the pipeline stages: Parse HTML, Recalculate Style, Layout, Update Layer Tree, Paint, and Composite Layers all appear as labeled tasks on the main thread timeline. You do not need to infer which stage is running; the panel names it.
Long Tasks, those exceeding 50 milliseconds, appear as red-flagged bars. Clicking into one reveals which pipeline stage or JavaScript call is responsible, down to the full call tree within the task. You can see not just that layout ran but what triggered it.
Forced reflow leaves a specific visual signature. In the flame chart, interleaved Layout and Recalculate Style blocks within a single JavaScript task indicate layout thrashing: the engine is being asked to compute layout, return a value, then invalidate and recompute it repeatedly inside one task. The console will also report violation messages with durations. The Performance Insights panel flags forced reflows exceeding 30 milliseconds explicitly, so you do not need to hunt for them manually.
Compositor thread activity appears on its own track. If an animation is running correctly on the compositor, it will appear in the Compositor track and JavaScript execution will not interrupt it. If you instead see animation-correlated work appearing on the main thread timeline, the animation is touching a layout or paint property and pulling work back onto the main thread. That is your signal.
The Layers panel, accessible through the three-dot menu in DevTools, renders the layer tree visually in 3D space. Over-promotion becomes immediately visible here; a page with hundreds of layers is a GPU memory problem waiting to surface on a constrained device. The panel also shows why a given layer was promoted, which tells you whether that promotion was intentional or a browser heuristic you may want to reconsider.
The pipeline is not a black box. Every stage is observable, every cost is attributable, and DevTools gives you the instrumentation to trace what you wrote to what the engine actually did.


