Skip to content

perf: self-host KaTeX and jupyter-matplotlib stylesheets - #125

Open
DrDrij wants to merge 1 commit into
mainfrom
perf/self-host-katex
Open

perf: self-host KaTeX and jupyter-matplotlib stylesheets#125
DrDrij wants to merge 1 commit into
mainfrom
perf/self-host-katex

Conversation

@DrDrij

@DrDrij DrDrij commented Aug 18, 2026

Copy link
Copy Markdown
Member

Both the KaTeX and jupyter-matplotlib stylesheets were render-blocking <link>s pointing at cdn.jsdelivr.net. This serves them from the site's own origin instead.

Why — reach, not milliseconds

jsdelivr is intermittently unreachable from mainland China, which is a significant share of the QuantEcon readership.

The failure mode matters here. When the CDN is blocked the maths markup still renders — MyST has already converted it at build time — but it arrives with no stylesheet. KaTeX depends entirely on CSS for layout, so fractions lose their bar and stack vertically, radicals lose their overbar, matrices collapse into run-together text, and superscripts sit inline. The lectures degrade worst exactly where the maths matters most, and they do so silently: nothing errors, the page just becomes wrong.

Serving from the site's own origin removes that dependency. Dropping a third origin from the critical path (a DNS lookup and TLS handshake before first paint) is a secondary benefit.

What changed

KaTeX. katex is already present in the tree, so this adds no new package — it becomes a direct dependency instead of a hoisted transitive one of myst-transforms / mermaid. A local app/links.ts replaces the KatexCSS export from @myst-theme/site, which is the thing that pointed at the CDN. The two routes that consumed it now import from ~/links; no upstream patch is needed.

Remix fingerprints the import and emits the stylesheet plus its 60 referenced font files into public/build/_assets/, served at /myst_assets_folder/ — the same publicPath every other bundled asset already uses, so no new path handling is introduced. Verified against a real remix build: all 60 fonts emitted and every url() correctly rewritten. Browsers still fetch only the handful of faces a page's glyphs actually need, not all twenty.

This also retires a version skew: the upstream export pins KaTeX 0.15.2 while this repo resolves 0.16.x. Worth being precise — that skew is not currently causing breakage. The two stylesheets are functionally equivalent for the pages checked (248 vs 250 rules, identical nine @font-face families), so this is hygiene rather than a bug fix.

jupyter-matplotlib. 316 bytes, vendored verbatim into the Tailwind bundle as styles/mpl-widget.css and pulled in by styles/app.css, so it now costs no request at all. It is kept rather than deleted: no current lecture renders ipympl output, but this theme is shared across lecture repos and any of them may enable %matplotlib widget. The one !important in that file is upstream's, preserved deliberately rather than silently "improved".

Removing only one of the two would have achieved little, since both sat on the same jsdelivr origin — the DNS and TLS cost is per origin, not per file.

The dependency change worth a second look

npm rejects an override that conflicts with a direct dependency of the same name, and katex already had a security override from #104. The fix is npm's own idiom:

"dependencies": { "katex": "^0.16.21" },
"overrides":    { "katex": "$katex" }

$katex makes the override defer to the direct version, so the >=0.16.21 floor still applies to every transitive consumer. Lockfile churn is a single line.

Related, not included

styles/app.css still opens with an @import to fonts.googleapis.com for Source Sans 3. That is the same class of problem and arguably a larger one — Google Fonts is blocked in mainland China, it affects body text on every page rather than just maths, and as a CSS @import it is discovered only after app.css parses, forming a serial request chain that cannot be preloaded.

Deliberately left out of this PR, and it is a larger change than a one-line swap. Scoping notes for whoever picks it up:

  • Tailwind does not rebase url() inside an @imported stylesheet (verified experimentally), so @import '@fontsource-variable/source-sans-3' in styles/app.css would emit font paths relative to the output file and 404. It needs the Remix import route this PR uses for KaTeX, where esbuild rewrites the URLs and emits the files.
  • @fontsource-variable/source-sans-3 declares the family as Source Sans 3 Variable, so tailwind.config.js and the inlined critical CSS in app/root.tsx both need the name updated — otherwise the first paint falls back to system sans and the FOUC returns.
  • The variable package is nonetheless the right choice: the theme uses seven distinct font weights plus italics, which the static package would ship as roughly fourteen files per unicode subset.

Because it touches app/root.tsx, it will conflict with this PR and with #123, so it is best done after both land.

Note for merge order

This and the Font Awesome removal (#124) both delete adjacent entries from the same links() array, so whichever merges second will show a trivial conflict. The resolution is to keep both explanatory comments and drop both <link> entries.

Both were render-blocking <link>s pointing at cdn.jsdelivr.net.

The motivation is reach, not milliseconds. jsdelivr is intermittently
unreachable from mainland China, which is a significant share of the
QuantEcon readership. When it is blocked the maths markup still renders
but arrives completely unstyled — fractions, radicals and matrices
collapse into run-together text — so the lectures are at their least
readable exactly where the maths matters most. Serving both from the
site's own origin removes that dependency. Dropping a third origin from
the critical path (DNS + TLS before first paint) is a secondary benefit.

- KaTeX: imported from the `katex` package so Remix fingerprints it and
  emits the 60 referenced font files under the usual
  `/myst_assets_folder/_assets/` path, alongside every other bundled
  asset. Browsers still fetch only the few faces a page's glyphs need.
  This also retires the upstream `KatexCSS` export from @myst-theme/site,
  which pins 0.15.2 while this repo resolves 0.16.x.
- jupyter-matplotlib: 316 bytes, vendored into the Tailwind bundle via
  styles/mpl-widget.css, so it costs no request at all. Kept rather than
  deleted — no current lecture renders ipympl output, but the theme is
  shared across lecture repos and any of them may enable it.

`katex` becomes a direct dependency instead of a hoisted transitive one.
The existing security override is rewritten as `"katex": "$katex"` so it
defers to that direct version, preserving the >=0.16.21 floor from #104.
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://QuantEcon.github.io/quantecon-theme.mystmd/pr-preview/pr-125/

Built to branch gh-pages at 2026-08-18 20:44 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

Copy link
Copy Markdown
Contributor

🎭 Visual regression results

passed  15 passed
skipped  1 skipped

Details

stats  16 tests across 1 suite
duration  32.6 seconds
commit  46239ca

Skipped tests

mobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › launch-colab

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant