Setting a Texture Memory Budget for Mobile

Part of GPU Memory & Texture Management in Performance Budgeting & GPU Architecture.

The problem

Frame time has a number everyone knows: 16.7 milliseconds. Texture memory has no equivalent, so it gets managed by vibes — a promotion here, a translateZ(0) there — until a mid-tier phone starts evicting textures and unrelated parts of the page flicker as they are re-rastered.

The flicker is the symptom people report; the cause is that nobody ever decided how much GPU memory the page was allowed to use. Unlike frame time, the ceiling is not enforced by the platform: the browser will happily allocate until it has to start throwing things away.

Root cause: the arithmetic is quadratic in pixel ratio

A compositor layer holds a rasterised copy of its element at device resolution, four bytes per pixel. The formula is short and unforgiving:

bytes = width × height × DPR² × 4

The square is what makes mobile the hard case. The same 360 × 240 card costs about 0.35 MB on a device pixel ratio of 1, 1.4 MB at DPR 2, and 3.1 MB at DPR 3. A full-viewport hero on a 412 × 915 phone at DPR 3 is roughly 13.6 MB on its own — more than a grid of thirty cards.

Two further costs hide behind the headline number. The layer tree itself has to be synchronised between the main thread and the compositor, so a page with hundreds of layers pays a per-frame bookkeeping cost even when nothing moves. And when memory runs short, the browser discards tiles and re-rasters them on demand, which converts a memory problem into a paint problem — the flicker.

Texture cost of one promoted elementEvery figure is the same four-bytes-per-device-pixel arithmetic; only area and pixel ratio change.Texture cost of one promoted element44x44 icon button, DPR 30.07 MB360x240 card, DPR 21.38 MB360x240 card, DPR 33.11 MB412x72 sticky header, DPR 31.07 MB412x915 full-screen hero, DPR 313.57 MB
Every figure is the same four-bytes-per-device-pixel arithmetic; only area and pixel ratio change.

Step-by-step resolution

Building a budget you can hold people toThe number matters less than having one written down and checked.Building a budget you can hold people to1Pick the weakest device you support and measure how much the page holds atrest.The Layers panel sums the estimates for you2Compute the peak: the maximum set of layers promoted at the same moment.Usually during a scroll through animated content3Set a ceiling below the point where eviction starts on that device.60 MB is a reasonable default for mid-tier phones4List the three largest layers and decide whether each earns its place.One hero often costs more than everything else5Record the budget next to the frame budget and check it in review.
The number matters less than having one written down and checked.

Instrumenting the budget in code

// Intent: estimate the texture cost of the elements this page promotes, so a
// regression shows up as a number rather than as a flicker on someone's phone.
const BYTES_PER_PIXEL = 4;
const BUDGET_MB = 60;

function estimateTextureCost(selector = '[style*="will-change"], [data-promoted]') {
  const dpr = window.devicePixelRatio || 1;
  let total = 0;
  const layers = [];

  for (const el of document.querySelectorAll(selector)) {
    const { width, height } = el.getBoundingClientRect();
    if (!width || !height) continue;                 // skipped or hidden
    const bytes = width * height * dpr * dpr * BYTES_PER_PIXEL;
    total += bytes;
    layers.push({ el, mb: +(bytes / 1048576).toFixed(2) });
  }

  layers.sort((a, b) => b.mb - a.mb);
  return { totalMb: +(total / 1048576).toFixed(1), dpr, largest: layers.slice(0, 5) };
}

// In a development build only: shout when the page exceeds its budget.
if (import.meta.env?.DEV) {
  const report = estimateTextureCost();
  if (report.totalMb > BUDGET_MB) {
    console.warn(
      `Texture budget exceeded: ${report.totalMb} MB at DPR ${report.dpr}`,
      report.largest
    );
  }
}
/* Intent: the cheapest way to stay inside the budget is not to promote
   off-screen content at all. */
.feed__row {
  content-visibility: auto;          /* skipped rows are never rastered */
  contain-intrinsic-size: auto 96px;
}

/* No static will-change anywhere: promotion is applied by the observer that
   owns its lifecycle, and released on exit. */

@media (prefers-reduced-motion: reduce) {
  /* Nothing will animate, so nothing should be promoted. */
  .feed__row { will-change: auto; }
}

Rendering Impact: the estimator reads getBoundingClientRect() for each candidate, which forces a layout — deliberately, once, in a development build only. It must never ship in a production interaction path.

The estimate is approximate by design: the browser may allocate tiles rather than one surface, may promote elements you did not ask for, and may discard tiles under pressure. What it gives you is a number that moves in the right direction when you do the right thing, which is enough to catch a regression in review.

What to trade when the budget is exceededWork down the list; the first two usually recover more than the rest combined.What to trade when the budget is exceededSavingCost to the designStop promoting the largestsurfaceVery highIts first animated frame maystutterSkip off-screen rows withcontent-visibilityHighNeeds accurate intrinsicsizingPromote on approach, releaseon exitHighOne observer to maintainAnimate a smaller overlayinstead of a heroMediumA design change, not just acode changeDrop will-change: opacitywhere transform is alreadypromotingLowNone — it was never a secondtextureReduce visual densityVariesThe last resort
Work down the list; the first two usually recover more than the rest combined.

Verification checklist

Constraints and trade-offs

  • The estimate cannot see implicit promotions caused by overlap, so the real figure is usually higher than the computed one.
  • Reading layout to estimate cost is itself expensive; keep the instrumentation in development builds.
  • A budget that is never checked is decoration — tie it to a review step or a development-mode warning.
  • Very low-end devices may evict well below a 60 MB budget; if that is your audience, measure rather than assuming.
  • Trading away a promotion trades memory for a stutter on the first animated frame, which is sometimes the right trade and should be a conscious one.

Frequently asked questions

How do I convert an element’s size into megabytes?

Width multiplied by height gives CSS pixels; multiply by the square of the device pixel ratio for device pixels, then by four bytes for RGBA. A 360 by 240 card at DPR 3 is 360 × 240 × 9 × 4 bytes, which is about 3.1 MB.

What is a safe budget for a mid-tier phone?

Treat 60 MB of animation textures as a working ceiling and 30 MB as comfortable. There is no specification number here — the real limit depends on the device, the other tabs and what the browser itself is holding — so the budget is a design constraint you enforce, not a value you can query.

Does a promoted layer that is off-screen still cost memory?

Usually yes, unless the browser has decided to discard the tiles. That is why demoting on exit matters, and why content-visibility: auto is so effective for long lists: a skipped subtree is never rastered in the first place.


Where the browser spends memory you did not ask for

A budget built only from your own promotions will under-count, sometimes badly, because several things promote elements without any declaration from you.

Overlap. Anything painted above a composited element must be composited too, or it would render underneath. One deliberate promotion in a densely layered component can produce three or four layers, which the Layers panel reports as an overlap compositing reason.

Scrollers. Browsers promote scroll containers so that scrolling can happen off the main thread. A page with several independently scrollable panels is holding textures for each of them before you animate anything.

Video, canvas and iframes. These always composite, at their full size, for their whole lifetime. A single autoplaying background video can exceed the entire budget you set for animation.

Fixed and sticky elements. Both are frequently promoted so they can be repositioned during scroll without repainting.

The practical response is not to fight these — most are the browser doing the right thing — but to measure the total rather than your contribution to it. Open the Layers panel on a real device profile, sort by memory, and read the top five entries. If three of them are things you never promoted, the budget for your own animations is smaller than you thought, and the largest saving may be removing a video rather than tuning a hover effect.