Skip to content

perf(Spinner): replace Web Animations API with CSS animation-delay sync#7550

Open
hectahertz wants to merge 4 commits intomainfrom
hectahertz/perf-spinner-css-animation-sync
Open

perf(Spinner): replace Web Animations API with CSS animation-delay sync#7550
hectahertz wants to merge 4 commits intomainfrom
hectahertz/perf-spinner-css-animation-sync

Conversation

@hectahertz
Copy link
Contributor

@hectahertz hectahertz commented Feb 15, 2026

Closes #

Replaces the Web Animations API-based spinner synchronization with a pure CSS animation-delay approach, eliminating a Safari/WebKit performance bottleneck.

The previous sync mechanism called three Web Animations API methods per Spinner mount: element.getAnimations() (3-5x slower in WebKit than Chromium), CSSAnimation.pause() + element.animate() (replaces native CSS animation with a JS-driven one, often falling back to main-thread compositing in WebKit), and Animation.startTime (forces WebKit to recalculate timing for all document animations). This resulted in 2 animations per SVG and a global useSyncExternalStore store that re-rendered all mounted Spinners when the first one set its startTime.

The new approach computes a negative CSS animation-delay from performance.now() at visibility time:

animationDelay = -(performance.now() % 1000)ms

Since all instances reference the same monotonic clock, they land at the same rotation angle regardless of mount time. The CSS animation engine handles everything natively (GPU-composited), with 1 animation per SVG and zero JS animation overhead. The delay is computed at visibility time (not mount time), so spinners using the delay prop don't flash at the wrong angle on the first frame.

Measurements (5 spinners, staggered mount)

Flag OFF Flag ON
main (Web Animations API) 276° spread, 1 anim/SVG 0° spread, 2 anim/SVG
This PR (CSS animation-delay) 276° spread, 1 anim/SVG 4° spread, 1 anim/SVG

The 4° spread is floating-point rounding from performance.now(), visually imperceptible.

Changelog

New

N/A

Changed

  • Spinner animation synchronization now uses CSS animation-delay instead of the Web Animations API

Removed

N/A

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

  1. Enable primer_react_spinner_synchronize_animations feature flag in Storybook toolbar
  2. Open the "Synchronized Spinners" example story
  3. Verify all spinners rotate in sync as they appear
  4. Toggle prefers-reduced-motion: reduce in OS settings, confirm sync is skipped
  5. Test in both Safari and Chrome

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Feb 15, 2026

🦋 Changeset detected

Latest commit: b8cc0c2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Feb 15, 2026
@github-actions
Copy link
Contributor

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the integration-tests: skipped manually label to skip these checks.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the Web Animations API-based spinner synchronization mechanism with a CSS animation-delay approach to eliminate a Safari/WebKit performance bottleneck. The previous implementation used multiple Web Animations API calls per Spinner mount (getAnimations(), pause(), element.animate(), and startTime manipulation) along with a global useSyncExternalStore that caused all mounted Spinners to re-render. The new approach computes a negative CSS animation-delay from performance.now() at visibility time, allowing the CSS animation engine to handle synchronization natively with GPU compositing.

Changes:

  • Removed the complex Web Animations API synchronization logic including the global animationTimingStore and useSpinnerAnimation hook
  • Implemented CSS-based animation sync using computed animation-delay based on performance.now() % 1000
  • Updated state management to track both visibility and sync delay, computing the delay at the moment the spinner becomes visible (either immediately or after the delay prop timeout)

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/react/src/Spinner/Spinner.tsx Replaced ~120 lines of Web Animations API code with a simple 4-line computeSyncDelay function; updated state management to compute sync delay at visibility time; applied computed delay as inline animationDelay style when feature flag is enabled and user has no motion preference
.changeset/spinner-css-animation-sync.md Added patch-level changeset describing the performance improvement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments