Reduce main bundle size by ~44% gzipped (732 KB → 412 KB) (#4229)

## Summary

Cuts the main JS chunk from **2,891 KB (732 KB gzip)** to **1,679 KB
(412 KB gzip)** by fixing two bundling issues and removing/replacing
heavy dependencies. Measured with a per-module `renderedLength` analysis
of the rolldown output (its prod sourcemaps are malformed, so
sourcemap-based tools misattribute sizes).

| Chunk | Before | After |
|---|---|---|
| `index-*.js` (min) | 2,891 KB | 1,679 KB |
| `index-*.js` (gzip) | 732 KB | **412 KB** |

## Changes

- **Sim worker moved out of the main bundle (~512 KB).** The
`?worker&inline` payload is now reached through a dynamic `import()`, so
it lands in its own lazy chunk fetched when a game starts. The worker
itself still uses Vite's inline Blob mechanism (with its `data:` URL
fallback) — runtime instantiation is byte-for-byte unchanged.
- **Replaced `lit-markdown` with `marked` + the already-bundled
DOMPurify (~380 KB).** lit-markdown transitively pulled sanitize-html,
htmlparser2, postcss, and two copies of entities into the client just to
render news markdown. New `src/client/Markdown.ts` matches its
image-stripping default.
- **Dropped `colorjs.io` (~114 KB).** It was only used for ΔE2000
distance in `ColorAllocator`; colord's lab plugin (already imported
there) provides the same CIEDE2000 via `.delta()`. Only relative
magnitudes are compared, so allocation behavior is unchanged.
- **`msdf-atlas.json` (~319 KB) fetched at runtime** like the atlas PNG,
preloaded in parallel with worker init in `ClientGameRunner` so
game-load latency is unaffected.
- **Tailwind CSS no longer shipped twice (~158 KB).** `o-modal` imported
`styles.css?inline`, duplicating the emitted stylesheet as a JS string.
It now adopts a constructed stylesheet built from the document's own CSS
(HTTP-cache hit in prod, `<style>` tags + HMR re-sync in dev) via
`SharedStyles.ts`.
- **Debug GUI lazy-loaded.** lil-gui + `gl/debug/*` now load on first
toggle (46 KB lazy chunk) instead of shipping in the main bundle.

Also looked at the `import * as d3` in RadialMenu (~84 KB) but left it:
rolldown tree-shakes the metapackage well and all but ~2 KB is the
genuine dependency closure of the selection/transition/shape/color APIs
in use.

## Test plan

- [x] `tsc --noEmit` clean
- [x] ESLint clean
- [x] Full test suite passes (1,374 + 65 tests)
- [x] `npm run build-prod` succeeds; worker/debug chunks present in
`asset-manifest.json` for the R2 upload
- [ ] Manual smoke test in dev: start a game (worker dev path), open a
modal (shared stylesheet), open news modal (markdown rendering)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-06-11 20:07:16 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 2789db8b96
commit 94f2293149
12 changed files with 174 additions and 226 deletions
+7 -19
View File
@@ -1,7 +1,6 @@
import { Colord, extend } from "colord";
import labPlugin from "colord/plugins/lab";
import lchPlugin from "colord/plugins/lch";
import Color from "colorjs.io";
import { PseudoRandom } from "../../core/PseudoRandom";
import { simpleHash } from "../../core/Util";
extend([lchPlugin]);
@@ -75,14 +74,12 @@ export function selectDistinctColorIndex(
throw new Error("No assigned colors");
}
const assignedLabColors = assignedColors.map(toColor);
let maxDeltaE = 0;
let maxIndex = 0;
for (let i = 0; i < availableColors.length; i++) {
const color = availableColors[i];
const deltaE = minDeltaE(toColor(color), assignedLabColors);
const deltaE = minDeltaE(color, assignedColors);
if (deltaE > maxDeltaE) {
maxDeltaE = deltaE;
maxIndex = i;
@@ -91,20 +88,11 @@ export function selectDistinctColorIndex(
return maxIndex;
}
/** Smallest delta-E 2000 distance from `lab1` to any of the assigned colors. */
function minDeltaE(lab1: Color, assignedLabColors: Color[]) {
return assignedLabColors.reduce((min, assigned) => {
return Math.min(min, deltaE2000(lab1, assigned));
/** Smallest delta-E 2000 distance from `color` to any of the assigned colors. */
function minDeltaE(color: Colord, assignedColors: Colord[]) {
return assignedColors.reduce((min, assigned) => {
// colord's lab plugin .delta() is CIEDE2000 normalized to 0..1; only
// relative magnitudes matter here.
return Math.min(min, color.delta(assigned));
}, Infinity);
}
/** Perceptual distance between two colors using the CIEDE2000 formula. */
function deltaE2000(c1: Color, c2: Color): number {
return c1.deltaE(c2, "2000");
}
/** Convert a colord color to a colorjs.io LAB color for delta-E math. */
function toColor(colord: Colord): Color {
const lab = colord.toLab();
return new Color("lab", [lab.l, lab.a, lab.b]);
}