Fix home-page SSR->CSR flicker#1287
Conversation
Angular 15 has no provideClientHydration; on every browser load Angular
tears down the entire SSR DOM and rebuilds the component tree from scratch.
Measured CLS = 0.89 at t=1.76s on /home (PerformanceObserver on dev-5).
The visible flicker is that ~600ms rebuild window between SSR view and
populated CSR view.
Two compounding causes, addressed in this PR:
1. CustomEagerThemeModule was commented out in src/themes/eager-themes.module.ts,
so every custom-themed wrapper (footer, header, root, ...) was lazy-loaded via
webpack code-splitting on the browser, stretching the gap. Re-enable it
(the existing custom/eager-theme.module.ts already declares the right set).
Bumps initial bundle by ~256KB; angular.json budget raised from 5MB to 8MB
to accommodate.
2. The bigger cause - no hydration - is masked by an inline pre-bootstrap script
in src/index.html that:
- Captures all <style ng-transition="dspace-angular"> blocks into
<style data-dspace-ssr-keep> tags Angular won't strip (Angular removes
the originals on bootstrap, which is why a naive overlay renders
unstyled).
- Moves (not clones) the SSR-rendered <ds-app> children into an
absolute-positioned overlay so they keep every live DOM/style detail.
- Hides the now-empty <ds-app> via a data-attribute and CSS rule.
- Exposes window.__dspaceRemoveSsrOverlay() for AppComponent to call
once ApplicationRef.isStable fires (with one rAF + 50ms pad).
- 15s safety fallback in case isStable never fires.
Bots and no-JS users still get the original SSR <ds-app> (the overlay is
JS-added). Real users see continuous SSR-rendered content while CSR rebuilds
invisibly underneath, then a 150ms fade reveals the CSR DOM in its final
data-loaded state.
Verified locally via Service Worker that suppresses the removal: overlay's
header height is 80px (proper styling preserved) versus 698px (the unstyled
fallback before this fix's style-preservation step).
Includes a small Windows cmd deploy helper at scripts/dspace-deploy.bat and
matching skill doc at .claude/skills/dspace-deploy/SKILL.md - multi-instance
safe local dev stack via the existing docker compose files.
There was a problem hiding this comment.
Pull request overview
Eliminates the visible SSR→CSR flicker on /home on this Angular 15 (pre-provideClientHydration) codebase. Achieved by (a) re-enabling the previously commented-out CustomEagerThemeModule so themed wrappers stop being lazy-chunked, and (b) installing an inline pre-bootstrap overlay in index.html that moves the SSR DOM into an absolute-positioned snapshot (with copies of <style ng-transition> blocks Angular would otherwise strip) while Angular invisibly rebuilds the real <ds-app>, then fades the overlay out from AppComponent when ApplicationRef.isStable settles. Adds a Windows-only multi-instance dev helper and bumps the production bundle budget to accommodate the eager-theme increase.
Changes:
- Re-enable
CustomEagerThemeModuleinsrc/themes/eager-themes.module.tsand raise theinitialbundle budget inangular.json(5MB → 8MB). - Add an inline SSR-mask overlay script + supporting CSS in
src/index.html, and a matchingremoveSsrOverlayWhenStable()hook inAppComponentthat runs outside Angular and waits onappRef.isStable. - Introduce a Windows cmd dev-helper (
scripts/dspace-deploy.bat+.claude/skills/dspace-deploy/SKILL.md) and ignore its generated artifacts (plus various local investigation files) in.gitignore.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/themes/eager-themes.module.ts |
Re-enables CustomEagerThemeModule so custom-theme wrappers ship in the main bundle instead of lazy chunks. |
src/index.html |
Adds inline overlay CSS + pre-bootstrap script that snapshots SSR DOM + ng-transition styles and exposes __dspaceRemoveSsrOverlay. |
src/app/app.component.ts |
Adds removeSsrOverlayWhenStable() that, after appRef.isStable, invokes the global overlay-removal callback. |
angular.json |
Raises initial bundle budget warning to 6MB and error to 8MB. |
scripts/dspace-deploy.bat |
New Windows cmd helper that generates env/override files and brings up a namespaced FE+BE+DB+Solr stack per INSTANCE digit. |
.claude/skills/dspace-deploy/SKILL.md |
Documents the deploy helper, URL conventions, and multi-instance contract. |
.gitignore |
Ignores per-instance docker/.env/override files and various local investigation artifacts. |
- angular.json: tighten budget back to 5.5MB warn / 6MB error (was 8MB) - index.html: re-entrancy guard on __dspaceRemoveSsrOverlay (null the pointer up-front so the isStable + 15s safety fallback can't double-fade) - index.html: drop aria-hidden from overlay so screen-reader users get the SSR snapshot during boot (ds-app underneath has visibility:hidden which already excludes it from a11y tree) - index.html: console.warn on the overlay-script catch so a silently broken flicker fix is at least diagnosable in DevTools - typings.d.ts: typed Window.__dspaceRemoveSsrOverlay augmentation; drop the `as any` cast in AppComponent.removeSsrOverlayWhenStable - app.component.spec.ts: cover removeSsrOverlayWhenStable (calls the global once on isStable=true; no-op when global absent) - Drop scripts/dspace-deploy.bat + .claude/skills/dspace-deploy/SKILL.md from this PR per request (local dev tooling, will live elsewhere)
|
Third take — apologies, the previous two were apples-to-apples but the wrong apples. Both prior clips used the same FE bundle (this PR's): the BEFORE just had the overlay-installer script stripped at the edge. That isolates Fix B but leaves Fix A (the re-enabled This take is the real-world comparison:
MP4 (better quality): https://raw.githubusercontent.com/dataquest-dev/dspace-angular/internal/fe-fix-home-page-flicker-evidence/flicker-comparison.mp4?v=3 On the left (BEFORE) you'll see: SSR'd LINDAT page paints → ~600 ms gap where the page goes to a stripped/half-built state as Angular tears down Use |
The overlay holds the SSR-rendered children alongside <ds-app>'s CSR-rendered children during the masking window. Cypress's cy.get(selector) sees both copies, so unique-id selectors return 2 elements and cy.click() fails. The overlay is purely a UX smoothing layer (no behaviour to E2E-validate), so short-circuit when window.Cypress is present. Browser users are unaffected.
PR Review Summary: Home Page SSR to CSR Flicker FixProblem This PR SolvesThis PR fixes a visible flicker on the home page during app startup. In simple terms, the page is rendered twice:
That visual jump is the issue this PR addresses. How the PR Fixes ItThe fix uses a temporary overlay to hide the rebuild phase.
Result: users keep seeing a stable page instead of a flicker. Why the Timeouts Are ThereThe timeouts are intentional and part of the UX fix:
These are not test-only hacks. They are defensive UI timing controls to avoid flicker and prevent overlay lock-in. VerdictThe approach is technically valid for Angular 15 environments where full hydration is not available. The timeouts are justified and expected in this type of anti-flicker strategy. |

Summary
Eliminates the visible flicker on
/home(reproducible on http://dev-5.pc:84/home; CLS 0.89 measured at t≈1.76s) that comes from Angular 15's lack ofprovideClientHydration— the whole SSR DOM is torn down and rebuilt on every browser load.Two compounding causes, both addressed:
CustomEagerThemeModulewas commented out insrc/themes/eager-themes.module.ts, so every custom-themed wrapper (footer, header, root, ...) was lazy-loaded via webpack code-splitting on the browser — stretching the gap. Re-enabled (the existingcustom/eager-theme.module.tsalready declares the right set). Bumps initial bundle by ~256 KB, soangular.jsonbudget raised 5 MB → 8 MB.The fundamental no-hydration rebuild is masked by an inline pre-bootstrap script in
src/index.htmlthat:<style ng-transition="dspace-angular">blocks into<style data-dspace-ssr-keep>tags Angular won't strip (without this the moved SSR DOM renders unstyled — the original styles are wiped on bootstrap).<ds-app>children into an absolute-positioned overlay so they keep every live DOM/style detail.<ds-app>via adata-dspace-ssr-hiddenattribute + CSS rule.window.__dspaceRemoveSsrOverlay()forAppComponent.removeSsrOverlayWhenStable()to call onceApplicationRef.isStablefires (onerAF+ 50 ms pad, then 150 ms fade).isStablenever fires.Bots / no-JS clients still get the original SSR
<ds-app>(the overlay is JS-added). Real users see continuous SSR-rendered content while CSR rebuilds invisibly underneath, then a 150 ms fade reveals the CSR DOM in its final data-loaded state.Files
src/themes/eager-themes.module.ts— enableCustomEagerThemeModulesrc/index.html— inline overlay script + supporting<style>src/app/app.component.ts—removeSsrOverlayWhenStable()hookangular.json— bundle budget bumpscripts/dspace-deploy.bat+.claude/skills/dspace-deploy/SKILL.md— local dev helper (cmd, no PowerShell), multi-instance safe via the existing compose files.gitignore— ignore the per-instancedocker/.env.dspace-*/docker/.override.dspace-*.ymlfiles the helper generatesTest plan
scripts\dspace-deploy.bat 7 rebuildbrings up FE+BE+DB+Solr;http://localhost:4007/homerenders fully styled with no white flash on reloaddocument.getElementById('__dspace_ssr_overlay')exists during boot and is removed after the app stabilisesview-source:of/homeshows the inline<style id="__dspace-ssr-overlay-style">and the bootstrap script — the page works without JS (overlay is JS-added, so bots / no-JS clients see plain SSR'd<ds-app>)/search,/browse/*,/admin/*etc) — the script early-returns when<ds-app>has no children