GPU Rasterization vs CPU Painting
Scrolling stutters and the profiler shows fat purple “Rasterize Paint” blocks on the raster worker threads instead of quick GPU submissions — the tiles for your layer are being turned into bitmaps on the CPU, in the rasterization phase that sits between paint recording and compositing.
This guide sits under Compositor Thread and Rasterization, part of the broader Compositing and GPU Acceleration area. “GPU rasterization vs CPU painting” is a slight misnomer that trips up a lot of engineers: painting (recording paint operations into a display list) always happens on the main thread, while rasterization (executing those operations to produce tile bitmaps) can run either on CPU raster threads via software Skia or on the GPU via Skia’s hardware backend. When the GPU path is unavailable or your paint ops force a fallback, raster work lands back on the CPU and eats your frame budget. This page is about spotting that fallback in a trace and dragging the work back onto the GPU.
Reproducing the CPU-Raster Stall
The fastest way to feel the difference is to record a large scrolling surface with a paint op that Chrome’s GPU raster backend handles poorly, then watch raster cost explode. A full-bleed hero with a big blurred backdrop is a reliable trigger, because heavy filter: blur() over a large area produces expensive raster tasks whichever backend runs them — and if GPU raster is disabled on the machine (old GPU, blocklisted driver, --disable-gpu-rasterization), every tile is rebuilt by software Skia on a worker thread.
<section class="hero">
<div class="backdrop"></div> <!-- 1600×900 blurred layer -->
<h1>Quarterly numbers</h1>
</section>
<style>
.hero { position: relative; height: 100vh; overflow: hidden; }
.backdrop {
position: absolute; inset: -40px;
background: url(/photo.jpg) center/cover;
filter: blur(24px); /* BAD: large-area blur rasterized per tile every invalidation */
will-change: transform; /* promotes a big layer whose tiles must all be rastered */
}
</style>
The filter: blur(24px) line is the culprit. It forces every tile covering the backdrop to run a separable Gaussian blur during rasterization, and because the layer is promoted and full-viewport, that is a lot of tiles. On a machine where the browser fell back to software rasterization you will see the raster worker threads saturate; on a GPU-raster machine you will still see cost, but it moves off the CPU and the frame recovers. The point of the repro is that the same paint op has wildly different cost depending on which backend rasterizes it.
Where Rasterization Runs: Threads, Queues, and Tiles
To fix the fallback you need a mental model of who does what. On the main thread, style and layout finish and the paint phase walks the layer tree, recording each layer’s drawing into a DisplayItemList — an ordered list of Skia paint operations. No pixels exist yet; this is the “painting” everyone means when they say CPU painting, and it is cheap relative to raster. The recorded lists are handed to the compositor thread (the Blink compositor, “cc”), which owns the tiling. cc divides each layer into a grid of tiles, decides which tiles are visible or soon-visible, and pushes a raster task for each dirty tile onto a queue.
Those raster tasks are where the two backends diverge. With software rasterization, tasks run on a pool of CPU raster worker threads that execute the Skia ops into a bitmap in shared memory. With GPU rasterization, cc instead records the ops into a command buffer and the GPU process (Viz/Ganesh or the newer Graphite backend) replays them on the GPU, writing straight into a GPU texture. Either way the finished tiles are handed to the display compositor, which turns visible tiles into textured quads and issues the draw. The choice of backend is per-page and reported in chrome://gpu; the cost difference is per-tile and shows up in the trace as the length of each raster task.
Notice that promotion decisions upstream directly change raster cost: a bigger promoted layer means more tiles, which means more raster tasks. If you are here because a will-change or translateZ(0) hint made things worse, the sizing problem is covered in promoting layers safely with translateZ, and the closely related idea of shrinking what has to be rerastered lives in minimizing paint areas with layer boundaries.
Reading the Rasterization Trace
A Performance recording tells you the backend without you having to guess. Expand the raster worker threads (labelled CrRendererMain for paint, and CompositorTileWorker rows for raster) and look at what sits under each frame. Software raster shows long Rasterize Paint / RasterTask blocks on the tile-worker rows; GPU raster shows short raster records on those rows plus GPUTask activity attributed to the GPU process. The tree below is an annotated software-fallback frame — the giveaway is that the raster tasks themselves are the long pole, not paint.
Frame 1024 (28.6 ms — over the 16.7 ms budget)
├─ CrRendererMain
│ ├─ Recalculate Style ............... 0.4 ms
│ ├─ Layout ......................... 0.6 ms
│ └─ Paint .......................... 1.1 ms ← recording is cheap
├─ Compositor
│ ├─ Commit ......................... 0.3 ms
│ └─ Prepare tiles .................. 0.2 ms ← queued 40 raster tasks
└─ CompositorTileWorker/1..4
├─ RasterTask tile(0,0) .......... 5.9 ms ← software Skia blur, one tile
├─ RasterTask tile(1,0) .......... 6.1 ms ← CPU path, no GPU offload
├─ RasterTask tile(2,0) .......... 5.8 ms
└─ … 37 more tasks saturating 4 workers ← this is the CPU-painting stall
Contrast that with a healthy GPU-raster frame, where the same layer barely registers on the workers because the blur executes on the GPU:
Frame 1024 (7.2 ms — within budget)
├─ CrRendererMain
│ └─ Paint .......................... 1.0 ms
├─ Compositor
│ └─ Prepare tiles .................. 0.2 ms
└─ GPU Process
└─ GPUTask raster+draw ........... 3.4 ms ← tiles rastered on GPU, workers idle
If chrome://gpu reports “Rasterization: Software only”, no CSS change will move work to the GPU — the machine or driver is the constraint, and your job is to make the paint ops cheap enough for the CPU. If GPU raster is on but one layer still stalls, the fallback is op-level: a paint operation the GPU backend cannot accelerate, or a layer so large it blows the max GPU texture size and gets split awkwardly, a limit explored in GPU memory limits in Chrome compositing.
The Fix: Keeping Tiles on the GPU Raster Path
There is no single switch; the fix is to remove whatever forces the CPU path and to shrink the raster surface so even a fallback is affordable. Two changes carry most cases: stop rasterizing an expensive large-area effect every frame, and stop promoting a layer bigger than it needs to be. The before/after below keeps the visual blur but makes it cheap to rasterize by isolating it, capping its promoted size, and letting the compositor treat it as static instead of a per-frame raster target.
/* BEFORE — large promoted layer, blur rasterized per tile on every invalidation */
.backdrop {
position: absolute; inset: -40px;
filter: blur(24px); /* rasterized across the whole viewport-sized layer */
will-change: transform; /* promotes a huge layer → many raster tasks */
}
/* AFTER — isolate paint, cap the layer, and let tiles be reused */
.backdrop {
position: absolute; inset: -40px;
filter: blur(24px);
contain: paint; /* clips paint to this box → fewer, bounded tiles */
content-visibility: auto; /* skips raster entirely while offscreen */
contain-intrinsic-size: 1600px 900px;
}
/* Promote only the small element that actually animates, not the backdrop */
.hero h1 {
will-change: transform; /* tiny layer → one or two GPU raster tasks */
}
The contain: paint line bounds the layer so tiling produces fewer tasks, content-visibility: auto skips raster for the backdrop whenever it scrolls out of view, and moving the promotion hint onto the small heading means the compositor is no longer re-rastering a full-screen blur. On a GPU-raster machine this keeps the blur on the GPU and idle; on a software-only machine it slashes the number of CPU raster tasks so the frame fits the budget either way. If your effect is an animation rather than a static backdrop, keep it on the compositor-friendly properties described in why transform and opacity are GPU accelerated, so the compositor transforms an already-rastered tile instead of asking for a fresh raster.
The table below maps each raster constraint to where it bites and what it costs, so you can decide which lever to pull first.
| Pipeline phase | Constraint | Cost when violated |
|---|---|---|
| Paint (record) | Display list grows with op count | Longer main-thread paint, larger commit |
| Tiling (cc) | One tile per ~256×256 region of a layer | Big layers fan out into many raster tasks |
| Raster (CPU) | Software Skia runs blur/shadow per tile | 5–7 ms per tile, workers saturate |
| Raster (GPU) | Texture size ≤ driver max; some ops unaccelerated | Per-op fallback to CPU, or split tiles |
| Draw (Viz) | Quads uploaded and composited | Cheap if tiles are ready; stalls if raster is late |
Verification Checklist
Frequently Asked Questions
Is CPU painting always slower than GPU rasterization?
No. Painting — recording paint operations into a display list — happens on the main thread regardless of backend and is usually cheap. What differs is rasterization: turning those ops into tile bitmaps. GPU rasterization is faster for large fills, gradients, and blurs, but for pages with tiny invalidations or simple ops the CPU path can be competitive and avoids GPU upload overhead. The pathological case is a large-area effect like a full-screen blur, where software raster costs several milliseconds per tile.
How do I know if my page is using GPU or software rasterization?
Open chrome://gpu and read the “Rasterization” line under Graphics Feature Status. “Hardware accelerated” means the GPU backend is active; “Software only” means every tile is rastered on CPU worker threads. Confirm at runtime with a Performance recording: GPU raster attributes work to the GPU process, while software raster shows long RasterTask blocks on the CompositorTileWorker rows.
Why did a single layer fall back to the CPU while the rest of the page uses GPU raster?
Rasterization backend can be decided per tile, not just per page. A layer falls back when it contains a paint operation the GPU backend cannot accelerate, or when the layer exceeds the driver’s maximum texture size and cannot be represented as GPU textures cleanly. Shrinking the layer with contain: paint or splitting the effect usually restores the GPU path for that layer.
Does will-change force GPU rasterization?
No. will-change: transform promotes an element to its own compositor layer, but the tiles for that layer are still rasterized by whichever backend the page uses. Promotion can actually increase CPU raster cost if the promoted layer is large, because more tiles need rasterizing. Promote the smallest element that moves, and keep expensive paint ops off it.
Related Guides
- Compositor Thread and Rasterization — the parent overview of how tiling and raster scheduling work.
- Promoting Layers Safely with translateZ — keep promoted layers small so raster stays cheap.
- GPU Memory Limits in Chrome Compositing — why oversized layers break the GPU raster path.
- Minimizing Paint Areas with Layer Boundaries — shrink the surface that has to be rerastered.
- Why Transform and Opacity Are GPU Accelerated — animate without asking for a fresh raster each frame.