Fix FOUC when navigating between hydrated client entries#11402
Merged
Conversation
Adopt server styles by refcount instead of resetting the adopted stylesheet on every reload. DOM preserved across a reload (e.g. inside a still-hydrated client-entry boundary) keeps its rules until the new module finishes loading and the virtual root re-renders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Preview Build AvailableA preview build has been created for this PR. You can install it using: pnpm install "remix-run/remix#preview/pr-11402&path:packages/remix"This preview build will be updated automatically as you push new commits. |
Contributor
|
The preview branch |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
diffNodesskips over) keeps its rules until the new module finishes loading and the virtual root re-renders, eliminating the flash of unstyled content.replaceServerStyles(source)on the style manager handles the prior/new diff, releasing only adoption refs of selectors absent from the next page — selectors held by an active css mixin (e.g. transient client-only state) are untouched.Root cause
frame.tsused to callstyleManager.reset()thenadoptServerStyles()beforediffNodes(). The synchronous block is fine for ordinary DOM, butdiffNodesdeliberately skips over hydrated client-entry boundaries and lets the hydration pass re-render them — that is an async step (module load). Between the style swap and the hydration re-render, the lingering previous-entry DOM had class names referring to rulesreset()had just dropped.Test plan
pnpm --filter @remix-run/ui run test— 756 pass / 0 fail, including the new regression test insrc/test/frame.test.tsxthat gates module loading to assert the prior entry's rule stays live while its DOM is still visiblepnpm --filter @remix-run/ui run typecheckoxlintclean on changed files/api/remix/ui/button/demos/basic/↔/api/remix/ui/button/demos/states/and confirm the preview frame swaps without a visible flash🤖 Generated with Claude Code