fix(ui): replace localStorage panel state restore with SvelteKit snapshot API to eliminate flash on load #115
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The document viewer restores panel state (open/closed, height, active tab) from
localStorageinside anonMount-gated$effect. This runs after the first render, causing a visible flash where the panel briefly appears in its default state before snapping to the user's saved state.Fix
Replace the
localStoragerestore logic with SvelteKit's native snapshot API, which restores state before the first render on browser back/forward navigation:For first-visit persistence (not just back/forward), keep a single
localStorage.getItemin the initial$statedeclaration:This removes the
localStorageRestoredflag and the guarded$effect.Acceptance Criteria
localStorageRestoredflag and related$effectguard are removed👨💻 Felix Brandt — Senior Fullstack Developer
Questions & Observations
$state(localStorage?.getItem(LS_KEY_OPEN) === 'true' ?? false)initialiser will throw on SSR becauselocalStorageis undefined server-side. Use thebrowserguard from$app/environment:snapshotexport must be at the top level of the<script>block in+page.svelte, not inside a function — SvelteKit requires it to be a module-level export. Double-check if the current code has it in the right place.localStorageRestoredflag and its guarding$effectshould be deleted entirely — not commented out, deleted. Per the clean code rules: no dead code.Suggestions
captureandrestorefunctions should be typed:🔒 Nora "NullX" Steiner — Application Security Engineer
Questions & Observations
sessionStorageunder the hood, notlocalStorage.sessionStorageis scoped to the browser tab and cleared when the tab closes — it doesn't persist across sessions, only across same-tab back/forward navigation. This is actually more private thanlocalStoragefor snapshot state, which is the right trade-off here.localStorageremains in use for first-visit persistence. The panel open/close state is not sensitive, so storing it inlocalStorageis fine. But worth being explicit in the code comment: if more sensitive state (e.g. document viewing position, annotation content) is added to this persistence layer later, it should be reviewed.localStorageandsessionStorageare readable by same-origin JS, and the state being stored (panel open, active tab, panel height) carries no sensitive data.No security findings — clean from my perspective.
🧪 Sara Holt — QA Engineer & Test Strategist
Test Strategy
The snapshot API requires a real browser navigation — this cannot be tested with Vitest unit tests. Playwright is the right layer.
E2E — Playwright:
Edge Cases to Cover
localStorage, not snapshotlocalStoragepersistsObservations
data-testidattributes to the panel and its toggle button if they're not already there — otherwise the Playwright test has no stable selector to work with.🏗️ Markus Keller — Application Architect
Questions & Observations
onMount+localStoragefor this was always a workaround for the lack of a better API. The snapshot API makes the intent explicit and moves the complexity into the framework where it belongs.localStorage?.getItem()optional chain does not protect against SSR — in server-side rendering,localStoragedoesn't exist at all and optional chaining on an undefined global won't help. Useimport { browser } from '$app/environment'as the guard. This is not optional.Suggestions
Snapshottype import from@sveltejs/kitmakes the export typed and checked at compile time — use it as Felix suggests.localStorageRestoredand its$effectentirely. Do not leave them commented out. The version history is in git.🎨 Leonie Voss — UI/UX Designer & Accessibility Strategist
Questions & Observations
Suggestions
🚀 Tobias Wendt — DevOps & Platform Engineer
No infrastructure concerns — pure frontend change, no deployment or config impact.
One build note: the
browserguard from$app/environmentis a SvelteKit build-time constant — it tree-shakes cleanly in production builds, so the SSR path has zero overhead from the guard. No performance concern there.The Playwright test for snapshot restore (back-navigation) will add a small amount of time to the E2E job. It's worth it — this is the only layer that can catch the flash regression.