Skip to content

chore(build): develop, build and e2e-test the web app with Turbopack (v2.39.2) - #201

Open
krazymir wants to merge 1 commit into
mainfrom
claude/webpack-turbopack-migration-7fa4a7
Open

krazymir wants to merge 1 commit into
mainfrom
claude/webpack-turbopack-migration-7fa4a7

Conversation

@krazymir

Copy link
Copy Markdown
Contributor

What & why

The web app now uses Turbopack instead of webpack for pnpm dev, the e2e dev server and the production build.

Under webpack, the e2e dev server kept every compiled route in the JS heap. The comments in the repo record the cost: a full warm-up took about 30 minutes and about 9.7 GB, and the CI agent OOM-killed the server mid-run. That is why Jenkins runs each shard as 5 passes, each on a fresh server.

I measured both bundlers locally. Each run started from an empty build folder, warmed every page and API route (about 250), and had an 8 GB heap cap. Heap is read after a forced GC.

Dev server Heap Private memory (process tree) Warm-up
webpack, current e2e settings (stopped at 132 of ~250 routes) 3,532 MB ~5.8 GB, still growing ~16 min for 132 routes (6–13 s per route)
Turbopack, default eviction ('auto') 1,521 MB 3.9 GB → 3.5 GB 237 s
Turbopack, 'full' eviction (the e2e setting) 1,518 MB 2.9 GB → 2.7 GB 282 s

On a second sweep with 'full', routes came back from Turbopack's disk cache without recompiling: 46 s for the whole sweep, median 182 ms per route. Per-route status codes were identical between runs.

Production build Time Peak working set
webpack 185 s (compile 70 s) 2.3 GB
Turbopack 97–117 s (compile 38 s) ~2.0 GB

Changes

  • Scripts: dev and build use Turbopack (packages/web/package.json). The Dockerfile, the GitHub Actions build job and Jenkins call these scripts. The Playwright webServer now starts next dev --turbopack itself.
  • e2e memory eviction: SKILLY_E2E_DEV_SERVER=1 replaces SKILLY_DEV_KEEP_ROUTES. The Playwright webServer, ci/e2e-shard.sh and the GitHub Actions e2e job set it. It turns on experimental.turbopackMemoryEviction: "full" (next.config.mjs), which only next dev reads. Two webpack-only settings are gone:
    • onDemandEntries, which Turbopack ignores;
    • the disabled memory-threshold restart. The heap now stays far below the restart line, and a restart would restore routes from the disk cache rather than recompile them.
  • webpack() hook removed. Its .js → .ts alias was dead weight: @skilly/shared is consumed from its compiled dist/, whose .js specifiers resolve as written.
  • sharp in the standalone output (outputFileTracingIncludes). Turbopack's file tracing follows only sharp's .node binary, not the libvips library that binary loads, so the first Turbopack build returned 500 on every route that imports sharp ("Could not load the sharp module"). Webpack's tracer had a sharp-specific rule for this. The include copies sharp's installed @img/sharp-* packages from the pnpm store.
  • .dockerignore skips the .next-e2e-* build folders. Turbopack's dev cache is about 1 GB per folder.
  • Comments in ci/e2e-shard.sh, ci/e2e-shard-plan.sh, the Jenkinsfile, global-setup.ts and playwright.config.ts now say which numbers were measured under webpack. Pass counts, warm-up scope and shard sizing are unchanged until a CI run on Turbopack re-measures them.
  • Spec: §2 names the bundler, and roadmap item 18 describes the e2e dev server's eviction.
  • Release: APP_VERSION 2.39.2, with a changelog entry.

Type of change

  • Bug fix
  • Feature / new behavior
  • Docs / infra only (no app behavior change). This is a bundler switch that should change nothing users see. The shipped bundle does change, so it carries a patch bump and a changelog entry.
  • Breaking change (install contract, API shape, required config)

Checklist

  • Spec: §2 Bundler plus an e2e dev-server bullet under roadmap item 18.
  • Version: 2.39.1 → 2.39.2.
  • Changelog: entry prepended.
  • Tests: no new logic. Existing suites pass: shared 583/583, web 243/243.
  • Green checks: pnpm --filter @skilly/shared build and pnpm typecheck pass, plus the tests of the touched packages (shared, web). I didn't re-run worker tests; the worker is untouched.
  • Invariants respected.
  • Commit subject: chore(build): … (v2.39.2).
  • No secrets in the diff.

Verification

  • Route tables: I built production with both bundlers. Both list the same 211 routes with the same static/dynamic marker.

  • Production smoke test: I ran both standalone builds with NODE_ENV=production, laid out the way packages/web/Dockerfile lays them out. Results were identical on every functional check:

    • /healthz and /readyz return 200;
    • the signed-out home page renders;
    • the CSP nonce is in the header and on every inline and src script, and rotates per request;
    • the security headers are set, including no-store on /api/*;
    • every referenced /_next/static asset returns 200;
    • /api/me and /api/skills return 401, and an admin API returns 403;
    • the Entra-only provider list is served, with no dev sign-in;
    • an unknown page returns 404.

    The only differences are chunk counts: 12 script tags under Turbopack against 18 under webpack.

  • Docker image: I built the image with Turbopack (node:22-alpine) and ran it. sharp resizes and encodes inside the container (musl libvips loads). /healthz and /readyz return 200, /api/skills returns 401, and every script carries the nonce.

  • Turbopack dev server: it boots, and it served every page and API route, including pages that import @skilly/shared, with no edge-runtime warnings in the log.

  • Static checks: ci/e2e-shard*.sh pass sh -n and bash --posix -n, and ci.yml parses.

For the reviewer

  • Not run: the Playwright suite, locally or in CI. The first CI e2e run on Turbopack will be this PR's (add the e2e label for GitHub Actions, or run the Jenkins stage).
  • Not deployed. The production bundle changes bundler. The smoke tests show the same behaviour as webpack, but a check after the next deploy is worth doing, especially icon upload, since sharp is the one dependency that needed a fix.
  • turbopackMemoryEviction is an experimental Next setting, used only by the e2e dev server.
  • Every machine that runs pnpm dev gets a Turbopack cache in packages/web/.next/dev/cache (about 1 GB, gitignored). A local .env.local that still sets SKILLY_DEV_KEEP_ROUTES is harmless; nothing reads it any more.
  • The Turbopack dev log shows a MaxListenersExceededWarning twice per full sweep. It comes from Next's own dev server (DevServer.getStaticPathsWorker), not from app code.
  • A follow-up could lower E2E_PASSES or move Jenkins to E2E_WARM=all once a CI run confirms the memory numbers on the 16 GB agent.

🤖 Generated with Claude Code

…(v2.39.2)

The e2e dev server ran webpack, which kept every compiled route in the JS
heap. A full warm-up took ~30 min and ~9.7 GB, and the CI agent OOM-killed
the server mid-run, hence the 5 passes per shard. Measured locally (cold,
every route warmed, 8 GB heap cap, heap after a forced GC): webpack had
reached 3.5 GB heap / 5.8 GB private memory by the halfway point, still
growing; Turbopack holds 1.5 GB heap and, with full memory eviction,
2.7-2.9 GB private memory for the whole sweep, warmed in under 5 min.

- `dev` and `build` use Turbopack. The Dockerfile and both CI pipelines call
  these scripts; the Playwright webServer now starts `next dev --turbopack`.
- SKILLY_E2E_DEV_SERVER=1 (Playwright webServer, ci/e2e-shard.sh, the GHA
  e2e job) sets `experimental.turbopackMemoryEviction: "full"`. It replaces
  SKILLY_DEV_KEEP_ROUTES, whose onDemandEntries setting Turbopack ignores
  and whose disabled memory-threshold restart is no longer needed.
- The `webpack()` extensionAlias hook is gone: @skilly/shared is consumed
  from its compiled dist/, whose ".js" specifiers resolve as written.
- outputFileTracingIncludes copies sharp's installed @img platform packages
  into the standalone output. Turbopack's tracer follows only sharp's .node
  binary, not the libvips library it loads, so without it every route that
  imports sharp answered 500 in production.
- .dockerignore skips the .next-e2e-* build folders (Turbopack's dev cache
  is ~1 GB per folder).
- Comments in the CI scripts, Jenkinsfile and e2e setup now say which
  numbers were measured under webpack; pass counts and shard sizing are
  unchanged until a CI run re-measures them.
- SKILLY_SPEC.md: §2 names the bundler; roadmap item 18 describes the e2e
  dev server's eviction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants