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.
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.
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.
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.
Related
- Chromium compositor behaviour for scroll timelines — the engine where layer waste is easiest to find
- GPU Memory & Layer Budgets — setting a budget from the tightest ceiling
- Scroll-Driven Media & Gallery Effects — where texture memory is actually spent
- Measuring frame rate across engines — and why it does not detect this failure