perf: bundle React in production mode

`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) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-20 06:52:21 +02:00
co-authored by Claude Opus 5
parent 23ad2ad797
commit a7cf59d8e4
2 changed files with 18 additions and 1 deletions
+17
View File
@@ -101,6 +101,23 @@ bun test --test-name-pattern "year-less"
BASE_PATH=/gacha-event-tracker/ bun run build 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 **Tests must never need build output.** They run before `bun run build` in CI; anything reading
`public/` must create its own fixture tree instead. `public/` must create its own fixture tree instead.
+1 -1
View File
@@ -9,7 +9,7 @@
"build:feed": "bun run scripts/build-feed.ts", "build:feed": "bun run scripts/build-feed.ts",
"refresh": "bun run scripts/refresh-sources.ts", "refresh": "bun run scripts/refresh-sources.ts",
"build:css": "bunx @tailwindcss/cli -i src/client/styles.css -o public/styles.css --minify", "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", "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", "dev": "bun run build && bun run serve.ts",
"build:static": "bun run scripts/build-static.ts", "build:static": "bun run scripts/build-static.ts",