GPU Memory & Layer Budgets
Compositor-driven animation trades memory for time: instead of re-rasterising an element every frame, the browser keeps a texture and moves it. That trade is excellent right up until the textures do not fit, at which point pages stop being slow and start being visibly broken — blank areas where content should be, or a stutter that no CPU profile explains. This topic covers what a layer costs, where layers come from, and how to set a budget that holds on the devices that actually matter. It sits under animation performance, profiling and optimization.
Guides in this topic
- Counting composited layers in DevTools — getting a number you can act on, and what to ignore.
- Reducing layer count on scroll-heavy pages — the changes that actually move the figure.
- View transition snapshot memory cost — the other large allocation, and why named elements are the budget.
Syntax reference
Nothing here is a single property; the budget is a consequence of several.
/* asks for a layer — and holds it for as long as the rule matches */
.card { will-change: transform; }
/* bounds what a repaint inside this element can affect */
.card { contain: paint; }
/* lets the engine skip rendering for off-screen sections entirely */
.section {
content-visibility: auto;
contain-intrinsic-size: auto 60vh;
}
/* a transition's named elements each cost two snapshots */
.hero { view-transition-name: hero; }
Minimal working example
The cheapest possible measurement, which needs no devtools and works anywhere:
// rough texture estimate for the elements you know are promoted
const dpr = devicePixelRatio;
const estimate = (el) => {
const { width, height } = el.getBoundingClientRect();
return (width * height * 4 * dpr * dpr) / (1024 * 1024);
};
const total = [...document.querySelectorAll('[data-promoted]')]
.reduce((sum, el) => sum + estimate(el), 0);
console.log(`~${total.toFixed(1)} MB of texture across marked elements`);
This undercounts, because it misses layers the page produced incidentally. Its value is that it can run on any device, including ones you cannot attach a profiler to, and it gets the order of magnitude right.
Where layers come from
The distribution matters more than the total. A page with fifteen layers, twelve of which are deliberate, has a design decision to review. A page with fifteen layers, three of which are deliberate, has a structural problem — and the fix for the other twelve is usually one change to how the chrome is composed rather than twelve separate ones.
Compositor-safe properties and the budget
Every compositor-safe property is a request for a layer. That is not a reason to avoid them — they are the reason the animation is smooth — but it does mean the budget is spent by the same decisions that make the page fast. The tension resolves in favour of fewer, larger, well-chosen animations rather than many small ones.
Two properties are worth calling out because they cost more than they appear to. backdrop-filter forces a layer and a relatively expensive render pass, and it is usually applied to persistent chrome, which makes it a permanent cost. And filter on a large element rasterises that element at full size before filtering, which for a full-bleed image is a substantial allocation on top of the image itself.
Setting a budget
A budget needs three things to be useful: a number, a device class it applies to, and a way of checking it that runs without anyone remembering. The number is the peak texture total for the busiest viewport of the busiest page. The device class should be the tightest one you support in real numbers rather than the most convenient one to test. And the check belongs in whatever automation already runs the accessibility assertions, because a budget nobody measures is a paragraph in a document.
As a starting point: keep the peak under roughly 100 MB at the device pixel ratio you are targeting, and treat any single layer over about 10 MB as something to justify individually.
Common implementation patterns
Pattern 1 — promote on interaction, release afterwards. Scope will-change to :hover and :focus-within so the allocation exists only while it is needed.
Pattern 2 — skip off-screen sections. content-visibility: auto with an intrinsic size removes whole sections from the peak rather than redistributing the cost.
Pattern 3 — cap the effect count on small viewports. Reducing how many elements animate simultaneously is far more effective than simplifying each animation.
Pattern 4 — size media for the slot. Texture memory is dominated by images, and an image served at twice its displayed size costs four times the memory it needs to.
Browser support and @supports guard
There is nothing to guard: layer promotion is not a feature that can be feature-detected, and every engine does it. What can be guarded is how much you ask for, which is the cost-limiting pattern rather than a capability query.
Gotchas and failure modes
-
Standing
will-changehints. A hint in a stylesheet that always matches is a permanent allocation for an element that animates occasionally or never. -
Overlap promotion nobody counted. One animating element under persistent chrome promotes everything above it. This is the largest single source of unexplained layers.
-
translateZ(0)left from an older codebase. It forces promotion, does not show up in a search forwill-change, and is usually no longer needed. -
Budgeting at 1x. The memory figure changes by nearly an order of magnitude between a development display and a phone. A budget set at 1x is not a budget.
-
Counting at rest. Layers appear and disappear as elements animate. The peak during a scroll is the number that has to fit, not the number at a standstill.
-
Treating eviction as a glitch. A blank area during a scroll on a phone is the budget being exceeded, not a rendering bug.
Performance checklist
- Measure the peak layer memory during a scroll, not at rest.
- Measure at the device pixel ratio you support, not the one you develop at.
- Audit every
will-changeand everytranslateZ(0)for whether it is still needed. - Look for overlap promotion under fixed and sticky chrome.
- Serve images close to their displayed size.
- Put the peak figure into automation so a regression is noticed without a manual profile.
Why memory rather than time is the constraint
Performance advice for animation is overwhelmingly about time: keep work off the main thread, stay inside the frame budget, avoid forced layout. All of that is correct and none of it explains the most common failure on modern animated pages, which is not slowness at all.
The reason is that compositor-driven animation is extremely cheap in time and not at all cheap in memory. Moving an existing texture costs the GPU almost nothing per frame; holding that texture costs the same amount of memory every frame whether it moves or not. A page that does everything right by the time-based advice — every animation on transform, nothing touching layout, no scroll handlers — can still be holding more textures than the device will give it.
When that happens the failure looks nothing like slowness. Chromium tends to degrade gracefully, redistributing work and dropping the occasional frame. WebKit on iOS discards textures instead, which produces a blank or half-drawn element for a frame or two and then recovers. Neither shows up in a CPU profile, and the second does not show up in a frame-timing measurement either, because the frames continue to arrive on schedule — they simply have nothing in part of them.
This is why layer budgeting deserves to be a first-class part of animation work rather than an optimisation applied afterwards. The decisions that spend the budget are design decisions: how many things animate at once, how large they are, how much persistent chrome the page carries. Those are much cheaper to change while the section is being designed than after it ships, and they are almost impossible to change once the design has been signed off on a desktop where the constraint does not exist.
What actually moves the number
Teams that decide to reduce layer memory usually start in the wrong place, because the intuitive candidates are not the expensive ones. In rough order of impact:
Media dimensions. An image is a texture, and the texture is sized by the pixels served rather than by the pixels displayed. A hero served at 2400 pixels wide into a 1200-pixel slot costs four times what it needs to, and it is frequently the single largest allocation on the page. Fixing the srcset on three heroes routinely halves a page’s peak.
The number of simultaneously animated elements. Ten cards animating at once cost ten layers whether the animation is a simple fade or an elaborate keyframe sequence. Reducing the count by staggering, or by animating a container instead of its children, changes the peak; simplifying the keyframes does not.
Persistent chrome. A fixed header, a sticky sidebar and a floating action button are three permanent layers, and anything with backdrop-filter is a permanent expensive one. They are easy to overlook because they are not part of any effect, and they are present on every page.
Overlap promotion. One animating element under that chrome promotes it all again in some engines. Restructuring so animated content does not sit beneath persistent overlays can remove several layers at once.
Legacy promotion hacks. translateZ(0) and backface-visibility: hidden were the standard way to force compositing before will-change existed. Codebases of any age still contain them, they no longer serve a purpose, and each one is a standing allocation.
Notably absent from this list: the complexity of the keyframes, the number of animated properties, and the easing function. None of them affect the memory at all.
Frequently Asked Questions
Is there a way to read actual GPU memory use from the page?
No. There is no API, and the figures in developer tools are the browser’s own estimates rather than measurements from the driver. That is why this topic recommends computing an estimate from element dimensions and pixel ratio: it is approximate, it is available everywhere, and it is right about the order of magnitude, which is the part that matters.
Does reducing layer count ever make things slower?
It can, and the trade is worth understanding. An element that is not promoted is re-rasterised into its parent whenever it changes, which for something that animates continuously is expensive. The rule of thumb is that anything animating continuously should be promoted and anything else should not — the waste is almost always in the second group.
How does this relate to the device’s total memory?
Loosely. The texture budget a browser will grant is a fraction of available system memory, adjusted for pressure from other applications and for the browser’s own policies. A device with a lot of memory and a great many open tabs can be tighter than a modest device with nothing else running, which is why the budget is expressed as “comfortably under” rather than as a target to hit.
Should the budget be per page or per viewport?
Per viewport, because that is what has to fit at any one moment. A long page with several animated sections may have a large total across the document and a perfectly modest peak, provided the sections do not overlap in the viewport and off-screen ones are skipped. Measuring the document total is misleading in both directions.
What is a reasonable layer count?
Fewer than twenty per viewport is comfortable on almost anything; beyond about thirty, most pages are carrying layers nobody chose. The count matters less than the total memory, but it is far easier to read at a glance and it correlates well enough to be a useful first signal — a page that has drifted from eight layers to thirty-five has usually acquired a structural problem rather than gained twenty-seven deliberate effects.
The count is also the number that regresses. Memory totals move with content and viewport size, which makes them noisy in automation; the layer count for a given page at a given viewport is stable enough that a change in it is nearly always a change in the code.
Does a service worker or cache affect any of this?
No. Textures are runtime GPU allocations rather than stored assets, so nothing about caching changes the peak. What caching does affect is decode timing on repeat visits, which improves how smoothly a media-heavy section starts — a real benefit, and a different one from the budget this topic is about.
Should designers know about this?
Yes, and framing it as a count rather than as bytes is what makes the conversation productive. “How many things move at once in this section, and how big are they” is a question a designer can answer and act on. “Peak texture memory in megabytes” is a question that gets delegated back to engineering, where the only remaining options are worse than changing the design would have been.
Does any of this apply to a page with no animation?
Some of it. Fixed and sticky chrome, backdrop filters and legacy promotion hacks all produce layers on a page that never animates anything, and a heavy image gallery holds textures regardless. Animation is what makes the budget worth thinking about, not what creates the cost in the first place.
Related
- Compositor-Safe Properties & will-change — the promotion mechanics in detail
- Chromium compositor behaviour for scroll timelines — reading the Layers panel
- Safari and WebKit performance notes — where exceeding the budget causes eviction
- Stacking cards with sticky positioning — the pattern that spends the most