Safari and WebKit Scroll Animation Performance Notes

WebKit runs scroll-driven animation on the compositor like the other engines, so the mechanics are the same. What differs is the ceiling and what happens when a page reaches it: rather than dropping frames, WebKit discards layer textures. The result is a page that measures as perfectly smooth and looks intermittently broken, which defeats the usual diagnostic instincts entirely. This guide covers that behaviour and the iOS-specific conditions around it. It sits under the engine support matrix.

When to use this approach

  • A media-heavy page that flickers on iPhone and is flawless everywhere else. This is the signature of texture eviction and effectively never has any other cause.
  • A section that behaves differently depending on what else the device has open. The texture budget is shared with other applications, which is why the same page on the same device can be fine one day and not the next.
  • Animations that stop entirely for some users. Low power mode is the usual explanation, and it is worth verifying before investigating anything else.
  • Reveals that fire at the wrong moment on mobile Safari specifically. The collapsing toolbar changes the scrollport height mid-scroll, which re-derives every view timeline range.
Texture eviction on iOS Under memory pressure WebKit discards layer textures rather than slowing down. The visible result is a blank or partially drawn element for a frame or two while it re-rasterises, which reads as a flicker rather than as a performance problem — and is why the usual profiling instincts do not find it. Within budget every layer holds its texture, frames are clean Over budget clean blank re-rasterised, clean again The frame rate never drops, so a frame-timing measurement reports a healthy page. The failure is visual and intermittent, which is why it is usually reported as "it flickers sometimes" rather than as slowness. Reproducing it reliably means testing on a device with other applications already open.

Implementation

1. Reduce media size before anything else

<img
  src="/media/hero-1200.avif"
  srcset="/media/hero-800.avif 800w, /media/hero-1200.avif 1200w, /media/hero-1800.avif 1800w"
  sizes="(max-width: 40rem) 100vw, 60rem"
  width="1800" height="1012" alt="">

Texture memory is dominated by pixels, and pixels are dominated by media. A source served at 1800 pixels into an 800-pixel slot costs more than twice the memory it needs to, on the device least able to spare it.

What actually helps on iOS Four mitigations ranked. Serving smaller images helps most, because texture cost is dominated by media. Reducing the number of simultaneously promoted elements is second. Dropping standing will-change hints is third. Reducing animation complexity helps least, because the animation itself was never the cost. effect on peak texture memory, largest first Smaller served images the dominant term Fewer promoted elements large Drop standing hints moderate Simpler keyframes negligible

2. Cap the number of simultaneously promoted elements

/* on small viewports, animate the first three cards and leave the rest static */
@media (max-width: 40rem) {
  .stack .card:nth-child(n + 4) {
    animation: none;
    will-change: auto;
  }
}

This is the one place where a viewport-based cut-down is genuinely appropriate rather than lazy: the constraint being worked around is real and correlates with viewport size.

3. Remove standing hints

.card:hover, .card:focus-within { will-change: transform; }

A permanent hint is a permanent allocation, and on a device where allocations are being evicted that is exactly the wrong trade.

4. Allow for the collapsing toolbar

.reveal {
  animation-timeline: view(block);
  /* generous enough to absorb the toolbar's height changing mid-scroll */
  view-timeline-inset: 12% 0;
}

An inset tuned to the exact chrome height is wrong for half of the scroll on iOS, because the chrome height is not constant. A proportional inset absorbs the difference.

5. Respect low power mode

// there is no direct query; treat a sustained low frame rate as the signal

Low power mode is not detectable directly, and attempting to infer it is unreliable. The robust approach is to build effects that are acceptable at a reduced frame rate rather than ones that depend on 60 Hz to read correctly.

Conditions that only exist on iOS Four factors. Low power mode reduces the frame rate and can suspend animation entirely. The collapsing browser toolbar resizes the scrollport mid-scroll. Rubber-band overscroll clamps progress at the ends. And memory pressure from other applications determines the texture budget your page receives. Low power mode reduces the refresh rate and can suspend non-essential animation outright Collapsing toolbar the scrollport changes height mid-scroll, so view() ranges re-derive as the reader moves Rubber-band overscroll progress clamps at the endpoints while the container springs back — correct, and looks like a pause Memory pressure from other apps decides your texture budget — it is not a fixed number

Verification

Test on a real device rather than the simulator. The simulator runs on the host machine’s memory and does not reproduce the ceiling that causes eviction, which makes it useless for exactly this class of problem.

Open several other applications first, then load the page. Eviction is a function of system-wide pressure, and a device fresh from a reboot is the least representative state to test in.

Scroll through the heaviest section repeatedly and watch for blank or partially drawn elements. Because frame timing stays healthy, the only reliable detector is your eyes — a frame-timing measurement will report the page as fine while it is visibly flickering.

Finally, enable low power mode and repeat. Effects that become unreadable at a reduced refresh rate need reworking regardless of what the memory situation is.

Edge cases and gotchas

Safari on macOS does not reproduce the iOS ceiling. It shares the engine and not the constraint, so a page that is fine in desktop Safari tells you nothing about the phone.

backdrop-filter is expensive in WebKit. It forces a composited layer and a relatively costly render pass. On a page already close to its budget, a frosted-glass header is a meaningful contributor.

Web inspector’s timeline does not show eviction. There is no event for it. The absence of an entry is not evidence that it did not happen.

Long pages with many promoted sections. Even when only one section is visible, layers for others may still be held. content-visibility: auto on off-screen sections lets the engine skip them entirely, which is one of the few tools that reduces the peak rather than redistributing it.

Home-screen web apps have a different budget. A page installed to the home screen runs in a different context with its own memory characteristics, so verify there separately if that is a supported entry point.

Browser-specific notes

This entire guide is WebKit-specific by design, but two points are worth stating in contrast.

Chromium under the same pressure tends to slow down rather than evict, so the same overloaded page produces dropped frames there — a symptom that is easier to measure and much easier to diagnose. Building to Chromium’s behaviour and assuming WebKit will degrade the same way is precisely the assumption that produces flickering iPhones.

Firefox sits between the two and, on desktop, rarely reaches either failure mode. Its mobile share is small enough that most teams verify it on desktop only, which is reasonable — but it means Firefox will not warn you about anything in this guide.

Frequently Asked Questions

How much texture memory does an iOS device actually allow?

There is no published figure, and it is not constant: the budget depends on the device, the iOS version, and what else is running. That is why this guide recommends reducing consumption rather than targeting a number — the only reliable strategy is to be comfortably under whatever the ceiling happens to be.

Does content-visibility: auto help?

Yes, and it is one of the more effective tools here. It lets the engine skip rendering work for off-screen sections entirely, which removes both their paint cost and their layers from the peak. The caveat is that it changes the scroll height calculation for sections whose size is not otherwise known, so pair it with contain-intrinsic-size.

Is this a reason to avoid scroll-driven animation on iOS?

No. The compositor path is if anything more valuable there, because the main thread is more constrained. The constraint is on how much you promote, not on whether you animate — and a page with two well-chosen effects performs beautifully on the same device where a page with twelve flickers.

Why does the page look fine in the simulator?

Because the simulator has the host machine’s memory. It reproduces layout, styling and behaviour faithfully and does not reproduce memory pressure at all. For this class of problem it is not a testing environment.

How do I convince a team that an iOS-only flicker is a real problem?

Record it. A short screen capture on a real device with a few applications open is far more persuasive than a description, because the failure is intermittent and easy to attribute to the device rather than to the page. Pair the recording with the layer memory figure from Chromium’s Layers panel at a three-times device pixel ratio, and the connection between the two becomes obvious.

The other framing that helps is device share. The devices that exhibit this are not old — mid-range phones bought new today have the memory characteristics that trigger it, and they are a substantial share of most audiences.

Is there a way to detect eviction from the page?

Not directly. There is no event and no API. What you can observe indirectly is a long paint or rasterisation immediately after a period of smooth scrolling, which is the re-rasterisation of a discarded texture — but distinguishing that from ordinary paint work is unreliable enough that visual inspection on a real device remains the practical detector.


Up: Animation Performance: Engine Support Matrix