Files

2.4 KiB
Raw Permalink Blame History

WebKit nested composite-transform frame pacing reproduction

This is a dependency-free reproduction for an iOS WebKit frame-pacing issue involving two nested animation clocks:

  • A route-sized parent surface whose translate3d() progress is updated by requestAnimationFrame.
  • A child with an infinite CSS rotate() animation, drawn as a rounded border spinner.

On the originating device, spinner durations within part of the 0.5s1.1s range produced regular 3334 ms rAF gaps, while durations outside that band were smooth. The workload per frame is effectively unchanged, making the duration-dependent behavior unexpected.

Run

Open index.html directly. The reproduction is a single, self-contained HTML file and works from a file:// URL without a server.

To test it through a local HTTP server instead, run this from the repository root:

npx vite external/bugs/composite-transform

This directory is independent of the Vue demo and has no build step or runtime dependencies. The CSS, test scene, controls, and measurement code are all in index.html, so that file can be attached directly to a browser bug report.

Protocol

  1. Do not scroll or touch the page while a sample is running.
  2. Run Sweep 0.51.1 s with rAF-driven selected and Spinner enabled checked.
  3. Repeat with Static translate3d parent.
  4. Repeat with No parent transform.
  5. Clear Spinner enabled and run the sweep as a control.

Each duration runs for 1.8 seconds. In the rAF-parent case, the incoming route moves for 700 ms and then remains stationary for the rest of the measurement.

Expected

Changing only animation-duration should alter angular velocity, not cause a stable 60 Hz page to begin missing display deadlines. The rAF stream should remain close to 16.7 ms throughout the sweep on a 60 Hz device.

Actual observation in the source application

The problematic sample showed:

  • A 16 ms baseline.
  • Repeated 3334 ms gaps during the route transition.
  • The same gaps continuing while the parent was stationary and the spinner remained visible.
  • Normal pacing after the spinner was removed from the DOM.
  • No route-loading delay, JavaScript long task, or layout shift correlated with the gaps.

The reproduction intentionally contains no Vue, Vue Router, Suspense, application store, network request, or service-worker caching logic in its measured path.