Skip to content

feat(bolt-vite-react-ts): upgrade to vite 8 - #132

Draft
sheepsteak wants to merge 4 commits into
mainfrom
chris/bolt-vite-react-ts-vite-8
Draft

sheepsteak wants to merge 4 commits into
mainfrom
chris/bolt-vite-react-ts-vite-8

Conversation

@sheepsteak

@sheepsteak sheepsteak commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Problem

bolt-vite-react-ts was on Vite 5 (vite ^5.4.2) until Vite 7 landed in #134. Vite 8 was blocked by a WebContainer-only problem: Rolldown can't load its native binary there, and its runtime fallback downloads @rolldown/binding-wasm32-wasi with pnpm on the first import. That download sat in front of every cold dev start, and /tmp doesn't survive a new boot.

Solution

Move the template to Vite 8.3.5 and @vitejs/plugin-react to ^6.1.2.

Ship @rolldown/binding-wasm32-wasi as a devDependency so Rolldown finds it locally and never runs the runtime download. On normal machines the native binary still loads first, so nothing changes there.

This follows the pattern from #137 (the Nuxt starter fix that bolt#8821 pointed at), with one difference: #137 also needed an overrides pin on @napi-rs/wasm-runtime, and this does not. See below.

Implementation Details

  • Versions: vite ^8.3.5, @vitejs/plugin-react ^6.1.2. Vite 8 bundles with Rolldown and Oxc, and uses Lightning CSS for build.cssMinify. css.transformer still defaults to postcss, so postcss.config.js and Tailwind 3 work unchanged.
  • Binding pin: @rolldown/binding-wasm32-wasi at exactly 1.2.16, matching the rolldown version Vite 8.3.5 resolves (~1.2.14). Rolldown's loader tries the native binary, then a locally installed binding, and only then the runtime download. The package has no cpu/os restriction, so it installs everywhere.
  • No overrides needed: the bug behind fix(nuxt): bundle rolldown wasm binding for webcontainer #137 was version skew. Bindings up to 1.2.15 pin @emnapi 2.0.0-alpha.5 with @napi-rs/wasm-runtime ~1.2.5, but 1.2.6 raised its @emnapi peer range to 2.0.0-beta.2, so an old binding plus a hoisted 1.2.6 failed with this.bridge.setLastError is not a function. Binding 1.2.16 ships @emnapi beta.2 and pins 1.2.6 itself, so the tree resolves consistently with no override. I confirmed the lockfile holds a single set (@emnapi/* beta.2, runtime 1.2.6) with no nested duplicate.
  • removeFileHash: keeps the anchored regex, because Vite 8 content hashes are base64url and can contain -.
  • Test: the iframe-based preview assertion and the setup() workaround came in with feat(bolt-vite-react-ts): upgrade to vite 7 #134 and are unchanged here.
  • Other bolt-* templates: bolt-slides is already on Vite 8. bolt-expo (Metro) and bolt-remotion (Webpack) don't use Vite. bolt-qwik stays on Vite 5, because Qwik 1.x declares vite: ">=5 <8".
  • Staying in sync: if vite is bumped later and rolldown moves off 1.2.16, the binding pin has to move with it. Rolldown checks the binding version matches.

Performance in WebContainer

Same harness as #134: @webcontainer/test with Vitest browser mode, headless Chromium, Apple Silicon, Nov 3 2026. A fresh Chromium and a fresh WebContainer per run, 4 runs per version, alternating order, medians with min-max.

Phase (median) Vite 5.4.8 Vite 7.3.7 Vite 8.3.5 vs Vite 7
npm install 2.25s (2.18-2.35) 2.40s (2.21-2.62) 2.35s (2.03-2.37) -2%
Cold npm run dev to styled first render 1.92s (1.90-1.98) 1.88s (1.85-2.02) 2.87s (2.60-2.91) +53%
Vite "ready in" on cold start 639ms (624-672) 596ms (560-712) 1.05s (1.03-1.06) +76%
HMR (new JSX plus a new Tailwind class) 46ms (45-49) 47ms (45-61) 47ms (45-48) 0%
Dev server restart to styled render 1.39s (1.33-1.44) 1.32s (1.31-1.35) 2.57s (2.31-2.60) +94%
npm run build (wall clock) 1.63s (1.61-1.65) 1.57s (1.56-1.68) 1.62s (1.60-1.67) +4%
Vite "built in" 579ms (557-593) 572ms (560-644) 494ms (470-521) -14%

The runtime download is gone. With the binding pinned, 0 of 4 runs logged Downloading @rolldown/binding-wasm32-wasi, and no pnpm invocation appears in the dev output. A control run with the devDependency removed logs the download and takes 7.16s to reach server-ready; with it, 1.52s.

What is left. Vite 8 is about 1s slower than Vite 7 to a cold styled render and about 1.2s slower on a dev server restart, even with no download. That is Rolldown and Oxc running as WASM plus Lightning CSS. HMR is at parity and vite build's own timer is faster by about 80ms, but wall-clock build time is about the same. Before this change the same measurement was 7.4s to server-ready and 8.9s to a styled render, so the pin recovers roughly 6s of the gap and leaves about 1s.

Where the numbers come from. "Styled first render" is a Playwright wait inside the preview iframe until the starter text is present and Tailwind's bg-gray-100 is the computed background. For the download check I spawned npm run dev directly and matched the dev output. The negative control used the identical script with only the devDependency removed, which is how I know the check detects a download rather than passing vacuously.

Test plan

  • npm ci, npm run build, npm run typecheck, tsc --noEmit -p tsconfig.node.json, npm run lint, and npm run package:check in bolt-vite-react-ts
  • test/bolt-vite-react-ts.test.ts in Chromium: production build, and the dev preview renders and updates on edit
  • Full CI Test workflow green (7 files / 14 tests). The first run failed on angular and bolt-expo npm install hook timeouts; a rerun passed those and all others. bolt-vite-react-ts passed on both runs.
  • WebContainer: no @rolldown/binding-wasm32-wasi download on install or on first dev start, verified against a control run
  • WebContainer timing compared against Vite 5 and Vite 7 (see above)
  • Firefox test project
  • New Bolt project from this template

Bump vite to ^8.3.3 and @vitejs/plugin-react to ^6.1.2 and regenerate
the lockfile. Make the build test tolerant of `-` in Vite's base64url
hashes and of readdir ordering.
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@sheepsteak
sheepsteak marked this pull request as ready for review October 13, 2026 14:45
@sheepsteak
sheepsteak marked this pull request as draft October 13, 2026 15:30
preview.getByText() and preview.getByRole() from @webcontainer/test return
locators scoped to the <iframe> element without entering it, and awaiting
them doesn't wait for anything, so the dev test passed without checking the
preview. Wait from Playwright's side through page.frameLocator() instead,
since the preview is cross-origin and in-page queries can't see into it.

With a real assertion the test failed on both Vite 5 and Vite 8: restoring
the setup() snapshot drops the executable bit from
node_modules/vite/bin/vite.js, so `npm run dev` exits with
`spawn vite EACCES`. Install in beforeEach instead of restoring the snapshot.
Move the template from Vite 7 to Vite 8.3.5 and plugin-react to 6.1.2.

Ship @rolldown/binding-wasm32-wasi 1.2.16, matching the locked rolldown.
Rolldown can't load its native binary in a WebContainer, and its runtime
fallback downloads the WASM binding on every boot. Installing it up front
removes that download: cold dev ready drops from ~7.4s to ~1.5s.

Vite 8.3.5 resolves rolldown 1.2.16, whose binding ships @emnapi beta.2
and pins @napi-rs/wasm-runtime 1.2.6, so the binding and runtime match
without an overrides entry.
Rolldown 1.2.16 resolves @emnapi 2.0.0-beta.2 with @napi-rs/wasm-runtime
1.2.6 on its own, so the binding loads without the pin. The runtime
download still happens on every new WebContainer, adding about 6s to the
first dev start, but the template no longer carries a version-pinned
devDependency that has to track rolldown.
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