Skip to content

FOUC: brief flash of unstyled / raw HTML on every page navigation #66

@mmcky

Description

@mmcky

Symptom

When navigating the site (changing pages), there's a brief flash of unstyled / raw-looking HTML before the page renders correctly. It happens on every page change. Observed on the deployed lecture-wasm site (built with myst build --html, theme v2.0.0).

Note: this is not the old hydration reload-loop (fixed in #63). The page renders fine once settled — the flash is a classic FOUC (flash of unstyled content) on navigation.

Root cause (two compounding factors)

1. Navigation is a full document load, not client-side (SPA) routing.
In the static build, internal links are plain anchors, e.g.:

<a href="/long-run-growth"></a>
<a href="/eigen-i"></a>

Clicking one destroys the JS execution context (confirmed via Playwright) — i.e. the browser loads a fresh document on every page change, so all CSS must re-apply each time rather than persisting across a SPA transition.

2. The critical render path includes render-blocking third-party CDN stylesheets.
Every page's <head> loads these remote stylesheets before it can paint a styled page:

https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css
https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css
https://cdn.jsdelivr.net/npm/jupyter-matplotlib@0.11.3/css/mpl_widget.css
  • font-awesome + jupyter-matplotlib are added by the theme in app/root.tsx links() (lines ~44–51).
  • katex CSS is injected by @myst-theme (the math path).

On every full-document navigation the browser must fetch these third-party resources on the critical path. Chromium's paint-holding hides this (it shows blank → styled in my repros), but other browsers / real-world network conditions paint the document before the remote CSS resolves, producing the brief raw/unstyled flash. Self-origin assets (the bundled /build/_assets/app-*.css) load fast and consistently; the remote CDN round-trips are the variable, flash-inducing part.

Suggested fixes (in priority order)

  1. Self-host the third-party CSS in the bundle (serve from the theme's public/, same-origin) instead of CDN <link>s — removes the cross-origin round-trips from the critical path of every page load. This is the high-leverage, low-risk fix:
    • font-awesome and jupyter-matplotlib: change the hrefs in app/root.tsx links() to local copies.
    • katex: it's injected upstream; bundle the matching katex CSS locally and suppress/override the remote link.
  2. Fix the KaTeX version mismatch while doing the above: the linked CSS is katex@0.15.2, but the theme pins/overrides katex to ^0.16.21 (package.json overrides). Bundle the matching 0.16.x CSS.
  3. (Optional, bigger) Inline critical CSS in <head> so the first paint is always styled even before the full stylesheet loads.
  4. (Deeper) Investigate client-side navigation so page changes don't reload the whole document (CSS would then persist across navigations and the FOUC would disappear entirely). This may be inherent to the MyST static (myst build --html) export — worth checking whether prefetch/SPA links are achievable.

Repro / evidence

  • Static page <head> contains the 3 remote stylesheets above (render-blocking).
  • Internal links are plain <a href> → full-page navigation (execution context destroyed on click).
  • Under network throttling, the document paint-holds (blank) until the remote CSS loads; under conditions where paint-holding doesn't fully engage, that same gap surfaces as the unstyled flash.

Environment

  • Theme: quantecon-theme v2.0.0
  • Consumer: lecture-wasm (myst build --html → static deploy)
  • mystmd: 1.9.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions