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. This guide sits under the Compositing and GPU Acceleration subsystem, and reads best alongside Layer Promotion and Composition when the VRAM pressure traces back to over-promoted layers.

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 subsystem is falling back to CPU-side rasterization, which blocks the compositor thread and introduces main-thread contention. Long scroll sessions are the usual trigger, so pair this with Scroll and Input Performance when the jank tracks the scrollbar.

The failure is a chain: allocations cross the per-process VRAM ceiling, cc starts evicting tiles, evicted tiles are re-rasterized synchronously, and the compositor stalls waiting for them — collapsing into software rasterization when the GPU pool cannot recover.

VRAM exhaustion failure chain Allocations crossing the VRAM ceiling trigger eviction, synchronous re-raster, a frame stall, and finally a software rasterization fallback. GPU memory pressure cascade Alloc > VRAM ceiling layer + tile buffers cc tile eviction drops cold tiles Synchronous re-raster raster worker blocks Frame stall > 16.6ms Software rasterization fallback

Debugging Workflow

Work top-down: confirm hardware acceleration is even active, capture a trace of the jank, correlate the offending layers with their VRAM cost, then audit the GPU process heap for retained buffers. Each step narrows the search before you touch code.

Four-step GPU memory debugging sequence A vertical sequence from baseline GPU state, to trace acquisition, to layer and memory correlation, to a process-level allocation audit. 1. Baseline GPU state chrome://gpu — confirm hardware accelerated, note Video Memory 2. Trace acquisition Performance panel + chrome://tracing (cc, viz, gpu, blink) 3. Layer and memory correlation Layers panel — flag layers over 2048x2048, match to will-change 4. Process-level allocation audit chrome://memory-internals — largest retained GPU buffers

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-internalsGpuProcess 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

Every mitigation below enforces the same rule: a layer holds VRAM only while it is actively animating or near the viewport. The lifecycle is promote late, demote early. Leaving will-change set after a transition completes is the single most common source of retained GPU buffers.

Layer promotion and demotion lifecycle An element enters the viewport, is promoted to its own layer, animates, then is demoted on transitionend so its VRAM is freed. Promote late, demote early Enters viewport no layer, 0 VRAM Promote will-change: transform Animate GPU-only frames transitionend rAF fires next tick Demote: will-change: auto layer discarded, VRAM freed

Post-animation demotion. Remove will-change or transform: translateZ(0) as soon as a CSS transition completes — the same discipline that keeps Animation Performance Patterns cheap. 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. The signature of a fixed page is a flat VRAM curve over a soak test and zero OutOfMemory events — the compositor holds a stable tile pool instead of thrashing eviction against re-raster.

Before and after VRAM soak comparison Before mitigation VRAM climbs until an out-of-memory event; after mitigation it holds a flat, stable curve across the soak. GPU memory across a 10-minute soak Before: retained layers OOM climbs until eviction fails After: demote on transitionend flat, stable tile pool

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. For wiring these observers into a repeatable harness, see Rendering Performance Metrics and Tooling.

Frequently Asked Questions

How much VRAM does a single composited layer actually cost?

A layer’s texture is allocated uncompressed at 4 bytes per pixel for RGBA. A 2048x2048 layer costs 16MB; a 4096x4096 layer costs 64MB. Retina and high-DPI displays multiply the backing store by the device pixel ratio, so a full-viewport layer on a 3x device can cost several times its CSS dimensions. Check exact sizes in the DevTools Layers panel.

Why does removing will-change too early hurt performance?

Setting will-change: auto mid-animation forces the compositor to discard the layer and re-promote it on the next frame, which triggers a re-raster and often a visible hitch. Demote only after the animation is finished — hook transitionend and clear it inside a requestAnimationFrame callback so the compositor has already presented the final frame.

What is the difference between tile eviction and software rasterization fallback?

Tile eviction is the compositor dropping cold tiles from the GPU pool to make room; those tiles are re-rasterized on demand and the frame stalls briefly. Software rasterization fallback is more severe — the whole process gives up on GPU raster and rasterizes on the CPU, blocking the compositor thread continuously until memory pressure clears. Eviction is recoverable jank; fallback is sustained jank.

Does OffscreenCanvas reduce compositor VRAM pressure?

It isolates the canvas’s rasterization onto a Web Worker so its buffers are accounted separately from the compositor’s tile pool, and it keeps heavy 2D/3D work off the main thread. It does not shrink total GPU memory, but it prevents a busy canvas from starving the compositor of tiles and stops main-thread contention during raster.

Which chrome:// pages confirm a GPU memory problem?

Use chrome://gpu to confirm hardware acceleration is active and to catch Out of Memory events, and chrome://memory-internals to inspect the GPU process heap and find the largest retained buffers. For frame-level detail, record chrome://tracing with the cc, viz, gpu, and blink categories and look for allocation events followed by eviction failures.