Debugging GPU Memory Limits in Chrome Compositing
Chromium’s compositor manages a tile-based VRAM pool for layer textures, intermediate render targets, and scroll-linked buffers. When cumulative allocations exceed per-process thresholds, the cc compositor triggers tile eviction. Evicted tiles are re-rasterized on demand — synchronously, on a raster worker thread — and the compositor must wait for them before submitting the next frame. In severe cases, the process falls back to software rasterization entirely. Both paths break the 16.6ms frame budget and are symptoms of the broader Hardware Acceleration Limits problem.
Symptoms and Root Cause
Sudden jank during heavy DOM mutations, large asset loads, or long scroll sessions. chrome://gpu reports GPU Process: Out of Memory or shows reduced Video Memory availability. The Compositing and GPU Acceleration subsystem is falling back to CPU-side rasterization, which blocks the compositor thread and introduces main-thread contention.
Debugging Workflow
1. Baseline GPU state
Open chrome://gpu. Confirm that Video Decode, Rasterization, and Compositing all report Hardware accelerated. Note the Video Memory and GPU Memory Buffer figures. If any capability shows Software only or Disabled, hardware acceleration has been blocked by a driver issue or GPU blocklist entry.
2. Trace acquisition
DevTools → Performance. Start recording, trigger the jank event, stop. In the flame chart, filter for:
LayerTreeHostImpl::UpdateTilePriorities— stalls longer than 2ms indicate tile priority recalculation overload.SkiaRenderer::PrepareTiles— fallbacks here mean Skia is preparing tiles in software rather than on the GPU.viz::GpuFrameSink::SubmitCompositorFrame— latency spikes above 8ms mean frame submission is blocked.
For deeper tracing, open chrome://tracing and record with categories cc, viz, gpu, and blink. Look for GpuMemoryBuffer::Allocate events followed by eviction failures.
3. Layer and memory correlation
Use the DevTools Layers panel. Look for layers with dimensions exceeding 2048×2048 pixels. A single 4096×4096 RGBA layer allocates 64MB uncompressed; two or three of those exhaust the mobile GPU budget entirely.
Cross-reference promoted layers with will-change declarations in the DOM. Layers that should no longer be promoted (post-animation, off-screen) consuming VRAM indicate a missing teardown.
4. Process-level allocation audit
chrome://memory-internals → GpuProcess heap. Identify the largest allocations. Layers with excessive dimensions or stacking contexts that prevent tile eviction appear here as retained GPU buffer objects.
Framework-Specific Mitigations
Post-animation demotion. Remove will-change or transform: translateZ(0) as soon as a CSS transition completes. In React and Vue, hook into the transitionend event:
element.addEventListener('transitionend', () => {
requestAnimationFrame(() => {
// Clear after the compositor has processed the final frame
element.style.willChange = 'auto'
})
}, { once: true })
Viewport-scoped promotion. Use IntersectionObserver with a rootMargin of 200–500px to promote only elements near the visible viewport, and demote as they scroll out:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(({ target, isIntersecting }) => {
target.style.willChange = isIntersecting ? 'transform' : 'auto'
})
},
{ rootMargin: '300px' },
)
Replace runtime CSS filters with pre-rendered assets. filter: blur() and filter: drop-shadow() force allocation of intermediate offscreen buffers for each composited element they apply to. Pre-rendered WebP or AVIF assets eliminate these buffers.
OffscreenCanvas for complex 2D/3D work. Moving heavy canvas rasterization to a Web Worker via OffscreenCanvas isolates VRAM consumption from the compositor’s tile pool:
const canvas = document.getElementById('visualization')
const offscreen = canvas.transferControlToOffscreen()
const worker = new Worker('render-worker.js')
worker.postMessage({ canvas: offscreen }, [offscreen])
Metric Verification
Re-run the Performance trace under identical conditions after applying mitigations. Validate:
| Metric | Target |
|---|---|
viz::GpuFrameSink::SubmitCompositorFrame latency |
< 4ms |
| GPU memory growth over 10-minute soak | Zero OutOfMemory events in chrome://gpu |
| Frame duration (95th percentile) | < 16.6ms |
| Dropped frames during continuous scroll or animation | 0 |
The PerformanceObserver longtask entry type captures main-thread blocks that result from compositor fallback. Use it alongside rAF delta timing to confirm the compositor is running cleanly after demotion teardown is in place.