Compositor Thread and Rasterization
Paint records what to draw; raster is where the drawing actually costs you. After the main thread hands a list of paint operations to the compositor, the compositor thread has to turn those vector-ish display lists into real pixels — and on a tall, image-heavy, or heavily-shadowed page that pixel-filling work is where a smooth 16.6ms frame quietly turns into a 40ms stutter. When users drag a long feed and see blank white bands flash in before the content paints, they are watching the rasterizer lose the race against the scroll offset. This guide walks the raster stage end to end: how painted layers become tiles, how the tile manager decides what to rasterize first, when Chrome uses the GPU versus a software raster path, and how to read the Performance panel’s raster track to prove where the time went.
This guide is part of Compositing and GPU Acceleration, the area of the pipeline that runs on the compositor thread after layer promotion has already carved the page into GPU-backed surfaces. If you have not yet confirmed which elements own their own layers, start there — raster cost is a function of layer count and layer area, so promotion decisions upstream set the ceiling for everything below.
From Painted Layer to GPU Tile: The Raster Stage
The compositor never rasterizes a whole layer as one giant bitmap. A promoted layer can be tens of thousands of pixels tall — a full-length article, an infinite feed, a virtualized table — and holding that as a single texture would blow past GPU memory limits and force a re-raster of the entire surface on every change. Instead, each layer is diced into a grid of fixed-size tiles, typically 256×256 device pixels. Raster runs per tile, so the compositor can rasterize only the tiles near the viewport, discard tiles that scroll far away, and re-raster just the tiles a paint invalidation touched. The unit of raster work is the tile, not the layer, and almost every optimization in this guide is really about rasterizing fewer tiles or cheaper tiles.
The stage sits downstream of paint. The main thread produces a display list (in Blink, a cc::DisplayItemList) describing draw operations — fill this rect, draw this glyph run, blur this shadow. That list is immutable and thread-safe, so it can be shipped to the compositor. The compositor’s tile manager slices the layer, assigns each dirty tile a raster task, and a pool of raster worker threads replays the relevant slice of the display list into a bitmap or a GPU texture. Only once a tile has a backing texture can the Draw step map it onto the screen quad.
Because the compositor thread is separate from the main thread, raster can proceed while JavaScript is executing — that decoupling is the whole reason transform and opacity animations stay smooth under main-thread load. But the compositor thread is not free. Its raster workers share GPU bandwidth and a finite raster task budget, and when they cannot keep up with new tiles the compositor is forced to draw a frame with missing tiles. The mechanics of that tiling step are covered in depth in How the Compositor Rasterizes Layers into Tiles.
Diagnostic Checklist: Spotting a Raster Bottleneck
Before touching code, confirm the symptom lives in raster and not in paint invalidation or layout. Open DevTools and run through these signals:
- Green flashes during scroll in the Rendering tab’s Paint flashing overlay mean regions are being repainted (and therefore re-rastered) while you scroll — a paint invalidation that keeps feeding the rasterizer new work every frame.
- Blank / checkerboard bands appearing at the leading edge of a fast scroll: the compositor drew a frame before the incoming tiles finished rastering. This is the clearest raster-can’t-keep-up signal.
- A tall “Rasterize Paint” / “Raster” track in the Performance panel, with
RasterTaskevents stacked across multiple worker lanes and extending past the frame boundary. - Dropped compositor frames — the Frames track shows partial or red frames even though the main thread is idle, meaning the delay is downstream of JavaScript.
- Huge layers in the Layers panel — a single composited layer measured in tens of megapixels multiplies tile count and raster time linearly with area.
will-change: transformsprayed across many elements, each forcing its own layer and its own tile set, is a common cause of a raster budget blowout — see when to use will-change without memory leaks.
Root Causes of Expensive Raster
Raster time is not one number; it is the product of how many tiles are dirty and how expensive each tile’s draw operations are. Four distinct causes dominate, and they call for different fixes.
Large layer area. A promoted layer that is 1200×20000 device pixels is roughly 375 tiles. If a paint invalidation dirties the whole thing, all 375 must re-raster. The fix is to shrink the invalidated area — see paint invalidation and regions — or to stop offscreen sections from rastering at all with content-visibility.
Expensive per-tile draw ops. Box shadows, blurs, filter, large border-radius with clipping, and gradients are pixel-shader-heavy. A tile full of box-shadow: 0 0 40px costs far more to raster than a flat fill of the same size, because every output pixel samples a blur kernel. Reducing shadow spread or replacing runtime blur with a pre-rendered image is often a bigger win than reducing tile count.
Raster churn during scroll. If content repaints on scroll — a sticky header recomputing, a parallax layer, a JS-driven position update — the rasterizer is fed fresh display lists every frame instead of reusing cached tiles. Keeping scroll handlers off the layout path, covered under scroll and input performance, removes this entirely.
Software raster fallback. When the GPU raster path is unavailable — blocklisted driver, exhausted GPU memory, or certain 2D canvas patterns — Chrome rasters on the CPU, which is 3–10× slower for the same tiles. The tradeoffs here get their own treatment in GPU Rasterization vs CPU Painting.
The Tile Manager and Raster Priority
Not every tile is equal. The tile manager assigns each tile a priority bin based on where it sits relative to the viewport and how soon it will be needed. Tiles that are currently visible get the highest priority; tiles just outside the viewport — the prepaint region the compositor speculatively rasterizes so scrolling has content ready — get the next bin; and tiles far offscreen are eventually evicted to reclaim GPU memory. During an active scroll, the priority is recomputed every frame against the projected scroll velocity, so the compositor tries to raster in the direction you are scrolling before you get there.
This is why a slow raster path shows up specifically as blank leading-edge bands: the tile manager knew which tiles it needed, queued them, but the raster workers did not finish before the compositor had to draw. The manager will draw the frame anyway rather than stall — a missing tile is rendered as the layer’s background color (usually white or a checkerboard in debug builds), which is exactly the checkerboarding artifact.
The practical lever here is the size of the prepaint region and how much raster work each tile carries. You cannot resize the prepaint ring from CSS, but you can make its tiles cheap so the workers keep up: fewer layers competing for raster workers, flatter draw ops, and offscreen sections skipped entirely. Reducing that competition on long pages is the whole subject of Reducing Raster Cost on Large Scrolling Pages.
There is a second dimension the tile manager tracks: raster quality. During an active fling the manager may schedule low-resolution or partial-quality tiles first so something draws inside the frame, then queue a high-quality re-raster of the same tiles once the scroll settles. This is why a very fast scroll on a heavy page can look slightly soft for a beat and then sharpen — you are watching the manager trade fidelity for frame timing. If you see that softening in the Layers panel as tiles flipping between quality states, it is a direct signal that your per-tile raster cost is too high for the scroll velocity, and the fix is the same: cheaper tiles, fewer layers, less offscreen work.
GPU Rasterization vs Software Raster
Chrome has two raster backends. GPU rasterization (the default on most hardware since Chrome 68) sends the display list to the GPU process, where Skia’s Ganesh/Graphite backend replays the draw operations directly on the GPU. Software rasterization runs Skia on the CPU raster worker threads and produces a bitmap in shared memory that is then uploaded to a GPU texture. GPU raster wins for pages dominated by fills, gradients, and transforms; software raster can occasionally win for pages of tiny, text-heavy tiles where the GPU upload overhead dominates, which is why Chrome still keeps the CPU path.
You can confirm which path is active by visiting chrome://gpu and reading the Rasterization line — it will say “Hardware accelerated” or “Software only”. A software-only status on a machine that should support GPU raster usually means a blocklisted driver or an exhausted GPU memory budget, which connects directly to the ceiling described in GPU memory limits in Chrome compositing.
The engineering takeaway is not “always force GPU raster” — you cannot force it, and some patterns silently drop you to software. The takeaway is to detect the fallback and remove its trigger. A single accelerated-2D-canvas readback, a filter the driver can’t accelerate, or GPU memory pressure from too many promoted layers can flip a page to software raster and quadruple its raster track, so the fix is usually upstream in layer or effect discipline rather than a raster flag.
It also matters where the raster time lands. GPU raster keeps the raster worker threads short and moves the pixel-filling into the GPU process, which competes with your other GPU work (compositing, video decode, WebGL) but frees the CPU for JavaScript. Software raster does the opposite: it consumes CPU raster-worker time and shared memory bandwidth, so on a mid-tier laptop already saturated by a heavy main thread, a software-raster fallback can starve both stages at once. That is why the same page can scroll acceptably on a workstation and checkerboard badly on a low-end device — the fallback threshold, not the source code, changed. Always profile the raster stage on the slowest device class you support rather than trusting a desktop trace.
Raster-During-Scroll and Checkerboarding
Checkerboarding is the visible failure mode of the raster stage, and it has a specific timeline. During a fling, the compositor advances the scroll offset every frame on its own thread. If the incoming tiles are already rastered (warm), the frame composites in under a millisecond and scroll is glassy. If the tiles are cold and the raster workers are still filling them, the compositor draws the frame with the layer’s solid background where content should be — a blank band — then paints the real content a frame or two later. The eye reads that as a flicker or tear at the scroll’s leading edge.
The trace of a checkerboarding scroll is unmistakable once you know the shape. In the Performance panel, the raster track shows RasterTask events packed back-to-back across all worker lanes, each frame’s raster spilling past the compositor’s Draw event, and the Frames track showing partial frames:
Compositor thread ── fling in progress
├─ Frame N Draw ................... 0.8ms [warm]
├─ Frame N+1 Draw ................... 0.6ms [blank band drawn]
│ └─ RasterTask (worker 0) ██████████ 6.2ms ← still running when Draw fired
│ └─ RasterTask (worker 1) █████████ 5.9ms
│ └─ RasterTask (worker 2) ██████████ 6.4ms
├─ Frame N+2 Draw ................... 0.7ms [content finally visible]
└─ symptom: 1–2 frame lag between scroll and content = checkerboard
The cure is to make the queued tiles cheap enough that raster finishes inside one frame. The single highest-leverage change is to stop rastering offscreen content at all. content-visibility: auto tells the browser to skip rendering — including raster — for subtrees that are outside the viewport, which is exactly what you want for a long feed. Here is the before/after:
/* BEFORE: every feed item is painted and rastered even far offscreen,
flooding the raster workers during a fling */
.feed-item {
/* no containment hint — the compositor tiles and rasters the
full layer height, ~20000px tall, all at once */
}
/* AFTER: offscreen items skip layout, paint, and raster entirely */
.feed-item {
content-visibility: auto; /* skips rendering work when offscreen */
contain-intrinsic-size: 0 420px; /* reserves height so the scrollbar
and scroll anchoring stay stable */
}
The mechanism: content-visibility: auto applies contain: layout paint (among others) to offscreen subtrees, and because paint is skipped, the compositor never generates a display list for those tiles, so the raster workers never touch them. The full pattern, including how to size the placeholder, lives in using content-visibility for offscreen content.
Step-by-Step: Cutting Raster Cost in DevTools
Follow this loop to isolate and fix a raster bottleneck. Each step names the exact panel and what a passing result looks like.
- Record a fling in the Performance panel. Open DevTools → Performance, click record, scroll a long page with a trackpad or the DevTools Rendering → Emulate a fast scroll if available, then stop. Expand the compositor thread and find the raster track.
- Confirm the raster track is the tall one. If
RasterTaskevents dominate and overrun frame boundaries, raster is your bottleneck. If the tall track is Update Layer Tree or Paint, jump to paint invalidation instead. - Turn on Paint flashing. DevTools → Rendering tab → check Paint flashing. Scroll again. Green rectangles that reappear every frame are re-raster churn; a scroll that shows no green (only the initial paint) is the goal.
- Turn on Layer borders. Same Rendering tab. Count the orange-bordered layers. If dozens of elements are promoted — often from stray
will-change— that is raster-worker contention; consolidate promotions. - Check the raster backend. Visit
chrome://gpu, read the Rasterization line. If it says “Software only” on capable hardware, you are paying the CPU raster tax; find the trigger (canvas readback, unaccelerated filter, GPU memory pressure). - Apply the highest-leverage fix. Usually
content-visibility: autoon the repeated list item, then re-shape any heavy shadows/blurs, then trimwill-change. - Re-record and compare the raster track. Success looks like every frame’s raster finishing well inside 16.6ms with no partial frames in the Frames track.
Heavy per-tile effects are the second most common culprit after layer area. Runtime blur is the worst offender because every output pixel samples a kernel:
/* BEFORE: a runtime blur re-runs the blur shader for every tile that
overlaps the card, every time the card re-rasters */
.card {
box-shadow: 0 8px 40px rgba(0,0,0,.25); /* wide blur radius = heavy raster */
backdrop-filter: blur(20px); /* forces a compositor readback + blur */
}
/* AFTER: a pre-rendered shadow image rasters as a flat texture sample */
.card {
border-image: url(/assets/card-shadow.png) 40 fill; /* one cheap texture blit */
/* backdrop-filter removed; use a semi-opaque solid where design allows */
background: rgba(238, 241, 247, .92);
}
The mechanism: a box-shadow blur and backdrop-filter are pixel-shader passes the rasterizer must execute per tile, while a pre-baked PNG is a single texture sample — the GPU’s cheapest operation. This trade sacrifices a little flexibility for a large raster-time cut on any tile the effect touches.
Edge Cases: React, Vue, and Next.js
Framework rendering models interact with raster in ways that are easy to miss because the framework work happens on the main thread while the cost surfaces on the compositor.
React virtualized lists. Libraries like react-window and react-virtual unmount offscreen rows, which is great for main-thread work but can fight the compositor’s own tile eviction: as you fling, React mounts new rows whose subtrees must be painted and rastered from cold on the main thread’s paint pass, then handed to raster. If you also apply content-visibility: auto, prefer the native approach for very long static lists and reserve JS virtualization for rows with heavy per-item JavaScript. Never combine an aggressive overscan with a promoted layer per row — that multiplies tile count. This overlaps with how React’s scheduler interacts with layout, covered in React concurrent rendering vs forced reflow.
Vue reactivity re-renders. A reactive dependency that changes on scroll (for example binding a :style transform to window.scrollY through a reactive ref) invalidates paint every frame, feeding the rasterizer non-stop. Drive scroll-linked visuals with a compositor-only transform and an IntersectionObserver rather than a reactive style binding; the related failure mode is detailed in Vue reactivity and layout thrashing.
Next.js hydration and next/image. On hydration, a long server-rendered page paints its full visible height at once, and if hero images decode late they trigger a re-raster of their tiles when the bitmap arrives. Setting explicit width/height (or sizes) so the layout box is stable, and letting next/image lazy-load below the fold, keeps offscreen image tiles from rastering until they approach the prepaint ring. Pair this with content-visibility on long content sections so hydration does not force the whole document’s tiles warm at once.
Transform-driven animations. Any animation that stays on transform/opacity is composited without re-raster — the tile texture is reused and only the transform matrix changes. Animating a property that changes the tile’s pixels (like box-shadow or background-position) forces re-raster every frame. The distinction is the entire point of why transform and opacity are GPU-accelerated.
Metric Targets
Use these targets to decide when the raster stage is healthy. Measure with the Performance panel raster track and the Frames track; confirm smoothness with the Event Timing and Long Animation Frames APIs surfaced under rendering performance metrics and tooling.
| Pipeline phase | Constraint | Cost target | How to measure |
|---|---|---|---|
| Raster per frame (scroll) | Must finish inside the frame | < 8ms across all workers | Performance panel raster track |
| Composited frame draw | Warm tiles only | < 1ms Draw |
Frames track, compositor lane |
| Checkerboard band frames | Blank leading edge | 0 partial frames per fling | Frames track (no red/partial) |
| Promoted layer count | Raster-worker contention | < ~20 active layers | Rendering → Layer borders |
| Largest composited layer | Tile count = area / 65536 | < 4 megapixels per layer | Layers panel |
| Raster backend | GPU path active | “Hardware accelerated” | chrome://gpu Rasterization line |
| Scroll responsiveness | Perceived smoothness | INP < 200ms during scroll | Event Timing API |
In This Topic
- How the Compositor Rasterizes Layers into Tiles — the tiling algorithm, display lists, and the raster-worker pool that turns paint records into textures.
- GPU Rasterization vs CPU Painting — when Chrome uses each backend, how to detect a software fallback, and the memory-copy cost of CPU raster.
- Reducing Raster Cost on Large Scrolling Pages — practical techniques for keeping raster inside the frame budget on tall feeds and tables.
Frequently Asked Questions
What is the difference between paint and raster?
Paint produces a display list — an immutable, thread-safe recording of draw operations (fill this rect, draw this glyph) — on the main thread. Raster is the compositor-thread stage that replays that display list into actual pixels stored in a tile texture. Paint decides what to draw; raster does the pixel filling. You can have expensive paint (many invalidations) or expensive raster (many/costly tiles) independently, and DevTools reports them on separate tracks.
Why do I see blank white bands when scrolling fast?
That is checkerboarding: the compositor advanced the scroll offset and had to draw a frame before the raster workers finished filling the incoming tiles. Rather than stall, the compositor draws the layer’s solid background where content should be, then paints the real pixels a frame or two later. It means raster is not keeping up with scroll velocity — reduce per-tile cost (shadows, blurs, layer area) or skip offscreen content with content-visibility: auto.
How do I know if Chrome is using GPU or software rasterization?
Visit chrome://gpu and read the Rasterization line under Graphics Feature Status. “Hardware accelerated” means GPU raster; “Software only” means Chrome is filling tiles on the CPU raster worker threads, which is typically 3–10× slower and adds a memory copy to upload the bitmap. A software fallback on capable hardware usually points to a blocklisted driver, an unaccelerated filter, a canvas readback, or GPU memory pressure from too many promoted layers.
Does content-visibility actually reduce raster work?
Yes. content-visibility: auto applies layout and paint containment to offscreen subtrees, and because paint is skipped the compositor never generates a display list for those tiles — so the raster workers never fill them. On a long feed this cuts the tiles rastered per frame to roughly the viewport-plus-prepaint region instead of the entire layer height. Always pair it with contain-intrinsic-size so the scrollbar and scroll anchoring stay stable.
Which DevTools tools diagnose a raster bottleneck?
Three, together: the Performance panel’s raster track (are RasterTask events overrunning the frame?), the Rendering tab’s Paint flashing overlay (is content re-rastering every frame during scroll?), and the Rendering tab’s Layer borders plus the Layers panel (how many and how large are the composited layers?). Add chrome://gpu to confirm the raster backend. A healthy scroll shows raster finishing inside 16.6ms, no green flashing after the first paint, and no partial frames.
Related Guides
- Compositing and GPU Acceleration — the parent area covering the whole compositor-thread pipeline this stage belongs to.
- Layer Promotion and Composition — how elements become GPU-backed layers, which sets the tile count raster has to fill.
- Scroll and Input Performance — keeping scroll handlers off the layout path so raster is not fed fresh work every frame.
- GPU Memory Limits in Chrome Compositing — the memory ceiling that forces tile eviction and software-raster fallback.
- Using content-visibility for Offscreen Content — the single most effective way to stop offscreen tiles from rastering.