mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-22 13:27:05 +00:00
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:
@@ -13,13 +13,20 @@ import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { ClientID, GameStartInfo, Turn } from "../Schemas";
|
||||
import { generateID } from "../Util";
|
||||
import { WorkerMessage } from "./WorkerMessages";
|
||||
// Inlined into the main bundle as a same-origin Blob, sidestepping the
|
||||
|
||||
// Inlined as a same-origin Blob (Vite's `?worker&inline`), sidestepping the
|
||||
// cross-origin `new Worker(url)` restriction that would otherwise apply when
|
||||
// the worker bundle is served from the CDN.
|
||||
import GameWorker from "./Worker.worker.ts?worker&inline";
|
||||
// the worker bundle is served from the CDN. The dynamic import keeps the
|
||||
// ~700 KB base64 payload in its own chunk, fetched when a game starts,
|
||||
// instead of inside the main bundle.
|
||||
async function createGameWorker(): Promise<Worker> {
|
||||
const { default: GameWorker } =
|
||||
await import("./Worker.worker.ts?worker&inline");
|
||||
return new GameWorker();
|
||||
}
|
||||
|
||||
export class WorkerClient {
|
||||
private worker: Worker;
|
||||
private worker: Worker | null = null;
|
||||
private isInitialized = false;
|
||||
private messageHandlers: Map<string, (message: WorkerMessage) => void>;
|
||||
private gameUpdateCallback?: (
|
||||
@@ -30,14 +37,7 @@ export class WorkerClient {
|
||||
private gameStartInfo: GameStartInfo,
|
||||
private clientID: ClientID | undefined,
|
||||
) {
|
||||
this.worker = new GameWorker();
|
||||
this.messageHandlers = new Map();
|
||||
|
||||
// Set up global message handler
|
||||
this.worker.addEventListener(
|
||||
"message",
|
||||
this.handleWorkerMessage.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
private handleWorkerMessage(event: MessageEvent<WorkerMessage>) {
|
||||
@@ -73,7 +73,11 @@ export class WorkerClient {
|
||||
}
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
async initialize(): Promise<void> {
|
||||
const worker = await createGameWorker();
|
||||
this.worker = worker;
|
||||
worker.addEventListener("message", this.handleWorkerMessage.bind(this));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = generateID();
|
||||
|
||||
@@ -84,7 +88,7 @@ export class WorkerClient {
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
worker.postMessage({
|
||||
type: "init",
|
||||
id: messageId,
|
||||
gameStartInfo: this.gameStartInfo,
|
||||
@@ -113,7 +117,7 @@ export class WorkerClient {
|
||||
throw new Error("Worker not initialized");
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
this.worker!.postMessage({
|
||||
type: "turn",
|
||||
turn,
|
||||
});
|
||||
@@ -137,7 +141,7 @@ export class WorkerClient {
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
this.worker!.postMessage({
|
||||
type: "player_profile",
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
@@ -163,7 +167,7 @@ export class WorkerClient {
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
this.worker!.postMessage({
|
||||
type: "player_border_tiles",
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
@@ -194,7 +198,7 @@ export class WorkerClient {
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
this.worker!.postMessage({
|
||||
type: "player_actions",
|
||||
id: messageId,
|
||||
playerID,
|
||||
@@ -228,7 +232,7 @@ export class WorkerClient {
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
this.worker!.postMessage({
|
||||
type: "player_buildables",
|
||||
id: messageId,
|
||||
playerID,
|
||||
@@ -274,7 +278,7 @@ export class WorkerClient {
|
||||
);
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
this.worker!.postMessage({
|
||||
type: "attack_clustered_positions",
|
||||
id: messageId,
|
||||
playerID,
|
||||
@@ -304,7 +308,7 @@ export class WorkerClient {
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
this.worker!.postMessage({
|
||||
type: "transport_ship_spawn",
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
@@ -314,7 +318,7 @@ export class WorkerClient {
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.worker.terminate();
|
||||
this.worker?.terminate();
|
||||
this.messageHandlers.clear();
|
||||
this.gameUpdateCallback = undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user