Skip to content

fix: shared-group global styles and eager keyframes registration#5696

Merged
quantizor merged 2 commits into
mainfrom
fix/shared-group-global-styles
Mar 21, 2026
Merged

fix: shared-group global styles and eager keyframes registration#5696
quantizor merged 2 commits into
mainfrom
fix/shared-group-global-styles

Conversation

@quantizor

Copy link
Copy Markdown
Contributor

Summary

Rearchitects createGlobalStyle to use shared stylesheet groups, fixing a recurring bug family spanning 7 years and 5+ issues (#5695, #3146, #3008, #3022, #2769).

Root cause: Each mounted instance of a createGlobalStyle component previously allocated its own stylesheet group (componentId + instanceNumber). This coupling between instance lifetime and stylesheet slot lifetime meant any mount/unmount cycle could scatter groups across the stylesheet or orphan them. The bug was "fixed" at least 3 times but kept reappearing.

Fix: All instances now share ONE group, registered once at definition time. An instanceRules cache enables selective removal via clear-and-rebuild.

Ordering algorithm

CSS injection order is now fully deterministic:

  1. When styled(), createGlobalStyle(), or keyframes() is called, a group ID is allocated from a global counter
  2. Lower group IDs always appear earlier in the stylesheet
  3. Render order does not affect CSS order — only definition order matters
  4. Global style instances share their definition's group, so remounting always returns to the original position
  5. Keyframes defined before a component now correctly appear before that component's rules

Changes

  • GlobalStyle.ts — shared group with instanceRules Map, computeRules helper, rebuildGroup (clear + re-insert survivors), rules-equal fast-path to skip CSSOM rebuild when CSS is unchanged, rehydration cache-seeding for static styles
  • Keyframes.ts — eager group registration via getGroupForId in constructor (uses GroupIDAllocator directly to keep native builds DOM-free)
  • createGlobalStyle.tsuseRef-based instance IDs, RSC per-instance style output from cache instead of shared group
  • Bundle size: 12.35KB → 12.4KB (+50 bytes, +0.4%)

Breaking changes

  • Keyframes CSS now appears before components that reference them (when keyframes are defined first). Semantically correct but changes observable order.
  • SSR rehydration markers changed from id="sc-global-X1" to id="sc-global-X". Apps upgrading with cached SSR output may see one-time style duplication until caches refresh.

Test plan

  • 568 tests pass across 53 suites (+30 new tests)
  • Multi-instance mount/unmount in every permutation order (2→1→0, 0→1→2, 1→0→2)
  • Definition order permutations (A→B→C rendered as C→A→B, B→C→A)
  • Rehydration: static and dynamic styles surviving instance unmount + prop change after
  • StrictMode double-render with static and dynamic interpolations
  • RSC: multi-instance, themed, global+styled in same tree, independent render passes
  • RSC coverage for StyleSheetManager, ThemeProvider, withTheme, checkDynamicCreation
  • Build passes with bundlewatch size check
  • Native build: 0 document. references (DOM isolation verified)
  • Performance validated via microbenchmarks (rules-equal fast-path saves ~530ns CSSOM ops at 4-7ns comparison cost)

Fixes #5695. Fixes #3146. Fixes #3008. Fixes #3022.

@changeset-bot

changeset-bot Bot commented Mar 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b621feb

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

This PR includes changesets to release 1 package
Name Type
styled-components Minor

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

@quantizor quantizor force-pushed the fix/shared-group-global-styles branch 3 times, most recently from 7bba068 to 3efd62c Compare March 20, 2026 20:54
@quantizor

Copy link
Copy Markdown
Contributor Author

This PR is testable via styled-components@6.4.0-prerelease.2, aliased to styled-components@test.

All instances of a createGlobalStyle component now share a single
stylesheet group, registered once at definition time. This fixes
unmounting one instance removing styles for other still-mounted
instances — a recurring bug family spanning #5695, #3146, #3008, #3022.

Keyframes now eagerly register their group ID at construction time,
so keyframes defined before a component appear before it in CSS output.

The ordering algorithm is fully deterministic: CSS injection order is
determined at definition time (when styled(), createGlobalStyle(), or
keyframes() is called), not at render time.
- Reorder sections: critical constraints first, reference material last
- Remove file listings (discoverable via filesystem, zero protective value)
- Condense performance patterns into table, remove negative results
- Extract mermaid rendering flow diagram to docs/rendering-flow.md
- Fix IS_RSC description: clarify build-time vs runtime behavior
- Consolidate duplicate server-detection and clearTag guidance
- Move misplaced server-detection bullet from attrs to build architecture
- 262 lines → 107 lines (59% reduction)
@quantizor quantizor force-pushed the fix/shared-group-global-styles branch from 5535d54 to b621feb Compare March 20, 2026 21:04
@quantizor quantizor merged commit 5b045df into main Mar 21, 2026
5 checks passed
@quantizor quantizor deleted the fix/shared-group-global-styles branch March 21, 2026 05:46
quantizor added a commit that referenced this pull request Mar 21, 2026
The keyframeIds Set, KEYFRAMES_ID_PREFIX constant, and
reconstructWithOptions copy were dropped when rebasing against
main (which merged #5696 with a different Sheet.ts).
quantizor added a commit that referenced this pull request Mar 21, 2026
The keyframeIds Set, KEYFRAMES_ID_PREFIX constant, and
reconstructWithOptions copy were dropped when rebasing against
main (which merged #5696 with a different Sheet.ts).
quantizor added a commit that referenced this pull request Mar 21, 2026
The keyframeIds Set, KEYFRAMES_ID_PREFIX constant, and
reconstructWithOptions copy were dropped when rebasing against
main (which merged #5696 with a different Sheet.ts).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant