fix(web): stop settings panels flashing on their first open - #1870
Merged
Conversation
Every settings tab flashed once on the first open after launch and then
stayed smooth, because three per-tab resources only load once per process
and all three were resolved while the panel was already on screen.
Lazy i18n namespaces were the main cause. Only WEB_UI_BOOTSTRAP_NAMESPACES
ship with the initial bundle, and `react.useSuspense` is off, so a panel
mounted ahead of its namespace renders raw i18n keys ("title", "subtitle",
"logging.sections.logging") and reflows into real copy once the JSON lands.
Map every tab to the namespaces it renders from and load them alongside the
tab's lazy chunk in preloadSettingsTabContent, so activation waits for both.
The settings nav does the same for its own `settings` namespace.
The Suspense skeleton also stole a frame: lazy() runs its ctor on first
render and throws a thenable even when the chunk is already cached, so
SettingsScene committed the loading skeleton before the panel. Wrap tab
activation in startTransition, matching what NavPanel already does for
scene navs. Cold entries into the scene (first open, deep links) preload
before their first paint instead of stepping through skeleton, then keys,
then content.
Finally, each config section reads its state over IPC and painted a 200px
"loading" block while doing so, which on a cold tab meant a page of
placeholders appearing and collapsing within a frame or two. Give
ConfigPageLoading a 200ms grace period and a short fade-in, so warm reads
land silently and only genuinely slow ones surface a placeholder.
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
Every settings tab flashed once the first time it was opened after launch, then stayed smooth on later opens. Three per-tab resources only load once per process, and all three were being resolved while the panel was already on screen:
WEB_UI_BOOTSTRAP_NAMESPACESship with the initial bundle andreact.useSuspenseis off, so a panel mounted ahead of its namespace renders raw i18n keys (title,subtitle,logging.sections.logging) and reflows into real copy when the JSON lands. Hitssettings/basics,settings/appearance,settings/editor,worktrees, and most other panels.lazy()runs its ctor on first render and throws a thenable even when the chunk is already cached, soSettingsScenecommitted its loading skeleton before the panel.NavPanelalready works around this withstartTransition; the settings nav did not.Changes:
settingsTabI18n.tsmaps each tab to the namespaces it renders from;preloadSettingsTabContentnow warms chunk and namespaces, so activation waits for both. Existing hover/focus preloading means a click is usually already warm.settingsContentRegistrytracks which tabs are fully warm (isSettingsTabContentReady).startTransition, so a cached-but-uninitialized lazy panel can no longer commit the skeleton.SettingsSceneholds only its cold first paint (first open after launch, deep links) until resources land. Tab switches never pass through that gate, so the existing "switch immediately, do not retain the outgoing panel" behavior is unchanged.nav-registryloads thesettingsnamespace together with theSettingsNavchunk, so sidebar labels no longer flash keys either.ConfigPageLoadingwaits out a 200ms grace period and fades in over 140ms (prefers-reduced-motionrespected), so warm reads land silently and only genuinely slow sections surface a placeholder.Type and Areas
Type: bug fix (UI/UX)
Areas: web UI (settings scene, config panels, component-library
ConfigPage, i18n preloading)Motivation / Impact
The settings surface looked unfinished on first use: opening any settings item after launch showed a grey skeleton, then untranslated i18n keys, then a page of collapsing loading placeholders, before settling. Users see each tab paint once, fully rendered.
Trade-off worth calling out: a cold tab now waits for its chunk and namespaces before painting instead of painting a placeholder immediately. In exchange for a slightly later first paint (a preloaded tab is unaffected), there is no multi-stage flash. This is the same trade-off
SettingsNavandNavPanelalready made for navigation.No API, config, or locale changes.
Verification
New regression test
settingsTabI18n.test.tspins the root cause and the fix: before preloading,t('title')echoes the key back (the visible flash); afterpreloadSettingsTabContent('basics')the namespace is resolved and real copy comes out. It also asserts the namespace map covers every tab inSETTINGS_CATEGORIES, so a new tab cannot silently regress.Two existing tests were adjusted to the new contract, with their original assertions intact:
SettingsScene.test.tsx— cold first render now waits for resources (chunk + JSON resolve off a macrotask, past whatact()flushes). The "switches immediately without retaining the outgoing panel" case keeps its single-microtask await and all three assertions, which is what proves switches are still synchronous.ExternalSourcesConfig.test.tsx— the test asserting the loading placeholder now advances fake timers past the grace period before looking for it.Reviewer Notes
settingsTabI18n.tsis hand-maintained and must stay in sync with theuseTranslation(...)/useI18n(...)calls in each panel. The coverage test catches a missing tab, not a missing namespace on an existing tab.ConfigPageLoadinggrace period does not eliminate every pathological case: a read finishing at ~250ms will still show a placeholder briefly. Bounding that would need a minimum-display duration, which felt like more machinery than the problem warranted.Checklist