From a7cf59d8e44f9fe4458e4061442edfaf3f6d38f5 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 20 Aug 2026 06:52:21 +0200 Subject: [PATCH] perf: bundle React in production mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build:js` was shipping React's development build. That cost 222 KB of the 566 KB bundle (61 KB of 164 KB gzipped), ran every element creation through dev-only validation, and left `StrictMode` double-invoking effects — so `fetchFeed` fired twice and every reader downloaded the feed twice on every single load. On a 4G/4×-CPU profile with the cache cold: first contentful paint 1480ms → 972ms, the feed on screen 1766ms → 1261ms, and two feed requests → one. The flag is `--production` and not `--define process.env.NODE_ENV='"production"'`, which is the obvious-looking version and produces a bundle that does not run at all: the define flips React to its production build while the JSX transform keeps emitting `jsxDEV`, so the page dies on `z is not a function` with a blank body. Both variants build clean, typecheck clean and pass all 843 tests, because nothing in the suite executes the bundle — so AGENTS.md now says to open a browser before believing this one. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 17 +++++++++++++++++ package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 775ba36..432057e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,23 @@ bun test --test-name-pattern "year-less" BASE_PATH=/gacha-event-tracker/ bun run build ``` +**`build:js` passes `--production`, and that flag is load-bearing.** Without it Bun bundles React's +*development* build: 566 KB rather than 344 KB (164 KB vs 103 KB gzipped), every element creation +validated at runtime, and `StrictMode` double-invoking effects — which in this app means +`fetchFeed` runs twice and every reader downloads the feed twice on every load. It shipped that way +until 2026-08-20; on a 4G/4×-CPU profile fixing it moved first contentful paint from 1480 ms to +972 ms. + +**Do not reach for `--define process.env.NODE_ENV='"production"'` instead.** It looks equivalent and +produces a bundle that does not run. The define flips React to its production build while the JSX +transform still emits `jsxDEV` calls into `react/jsx-dev-runtime`, so the page dies on +`jsxDEV is not a function` — minified to `z is not a function`, which is what it looks like in a +built copy. `--production` switches the transform *and* the env together, which is why it is the +only one of the two that works. Setting `NODE_ENV=production` in the environment has the same defect +as the define. **A change here must be loaded in a browser, not merely built** — both broken variants +build clean, typecheck clean and pass all 843 tests, because nothing in the suite executes the +bundle. + **Tests must never need build output.** They run before `bun run build` in CI; anything reading `public/` must create its own fixture tree instead. diff --git a/package.json b/package.json index e8b1c60..bd790c5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build:feed": "bun run scripts/build-feed.ts", "refresh": "bun run scripts/refresh-sources.ts", "build:css": "bunx @tailwindcss/cli -i src/client/styles.css -o public/styles.css --minify", - "build:js": "bun build src/client/main.tsx --outfile public/main.js --minify", + "build:js": "bun build src/client/main.tsx --outfile public/main.js --minify --production", "build": "bun run build:feed && bun run build:css && bun run build:js && bun run build:static", "dev": "bun run build && bun run serve.ts", "build:static": "bun run scripts/build-static.ts",