Adopt Rspack: proposal & rollout plan
Summary
Replace both Webpack and Vite with Rspack — a Rust-based, webpack-API-compatible bundler — as the single engine for dev, prod, and CI. Today Webpack runs everywhere (default dev, prod, CI) and Vite is an opt-in, faster dev experience; Rspack consolidates on one fast engine.
Problem
- Webpack (prod + default dev) is single-threaded and slow at our scale: multi-minute builds, heavy HMR, high memory.
- Vite is the faster dev option but isn't the default, because its prod viability is
unproven for us: Rolldown fragments into many small chunks, and this is inherent to the
engine — its chunking controls (
advancedChunks) don't yield a suitable bundle for our hundreds of entrypoints. We won't standardize on a dev engine we can't commit to for prod. - Result: multiple bundlers, dev output that doesn't guarantee prod, and a multiplied tooling surface.
Proposal
Adopt Rspack everywhere in one decisive switch, keeping the webpack mental model (config,
loaders, plugins, splitChunks) so migration and retraining cost stay low.
- Single-step switch: once prerequisites land, enable Rspack for dev + prod + CI and disable Vite in one move — no flag-gated Webpack/Vite coexistence to maintain.
- Webpack stays in-tree but inactive, then is removed in a separate follow-up once the switch is proven in production.
- Vite is disabled, not deleted: its integration stays dormant as a fallback should Rolldown later ship prod-viable chunking.
- Out of scope: no framework or module-system change (no Vue→React).
Why Rspack
vs Vite — the deciding factor is chunking, not raw speed (Vite 8/Rolldown is fast).
Rspack reuses our existing splitChunks/cacheGroups and lands at request-parity with
today's Webpack output. Rolldown can't: it fragments into many small chunks regardless of
configuration (advancedChunks doesn't fix it) — the unsuitable bundle is inherent to the
engine, not our chunking config. Measured cold-load impact under Metrics.
vs staying on Webpack — we're stuck on Webpack 4: single-threaded, slow, and memory-hungry (it builds near the 10 GB heap ceiling — a real cost in CI), and the Webpack 5 migration has stalled. Rspack keeps the same config model but is webpack-5-API-compatible, fast, and lighter on memory, getting us off Webpack 4 without finishing that migration.
vs Rsbuild — Rsbuild is an opinionated layer over Rspack that owns the config and
exposes raw access only through escape hatches. Our load-bearing pieces (the Vue 2/3
infection, hand-tuned splitChunks, the Rails-consumed manifest) would all live in those
hatches while we suppress Rsbuild's HTML/CSS/chunking defaults — a second migration for no
gain. Official guidance agrees: use Rspack directly when migrating from webpack. Revisiting
Rsbuild is a follow-up gated on the config simplifying (post-Vue-3, post-CSS-in-bundler).
Compatibility — @rspack/core/@rspack/cli are drop-ins; standard loaders and most
plugins work unchanged (~90–95% ecosystem coverage). Adaptations made on the branch: the
Vue 2/3 @vue/compat infection (see Risks), replacing vue-loader@15's crashing
VueLoaderPlugin with a custom matchResource plugin, and minor asset-naming parity.
Ecosystem — Rspack anchors the same Rust toolchain as Vite: Rstest/Rslint (Jest-/ESLint-compatible) stay available as low-cost follow-ons.
Plan
Rollout is an epic of child issues. The foundation is MR !242376 (closed) (the spike): the full
webpack→Rspack migration, green on
pipeline 2636800017
(32 FOSS + 20 EE system pg17 shards, 0 non-allow failures), Rspack opt-in via
ENABLE_RSPACK. Merge first, then:
- GDK dev-server integration — upstream the HMR WebSocket-upgrade fix into GDK's
nginx template so it survives
gdk reconfigure. Prereq for #604385. - Switch to Rspack — enable Rspack for dev + CI and disable Vite, in one step. Depends on #604383 (closed).
- Remove Webpack — delete config, deps, and CI jobs once the switch is proven. Depends on #604385.
- Rsdoctor in CI — bundle-size budgets and regression guards. Depends on #604385.
Metrics
Webpack 4.47.0, Vite 8.0.16 (Rolldown), Rspack 2.1.0-rc.0. Local = one machine (Apple Silicon), cold caches, median of 3. CI = gitlab-org/gitlab, cold cache, with job IDs. "Build" = production assets.
Build & output (local)
| Metric | Webpack | Vite | Rspack |
|---|---|---|---|
| Cold prod build | 1671s (n=1) | 63.3s | 64.6s |
| Peak memory (RSS) | ~9.6 GB | ~6.5 GB | ~7.4 GB |
| Emitted JS (raw / gzip) | 224 / 59 MB | 52 MB / — | 160 / 44 MB |
Rspack is ~26× faster than Webpack locally (matching the ~20× CI result) and emits less
JS. Vite's smaller raw total reflects different chunking and no @vue/compat duplication;
it ships no precompressed .gz.
CI assets-compile (cold)
| Job / measure | Webpack 4 | Rspack | Speedup |
|---|---|---|---|
compile-production-assets — bundler time |
~1838s | 86.4s | ~20× |
compile-production-assets — total job |
2222s | 554s | ~3.8× |
compile-test-assets — bundler time |
~214s | 52.9s | ~3–4× |
Bundler time = the bundler's own compile; total job also includes clone/bundle/cache/
image-build. Evidence: Rspack pipeline 2636800017, Webpack master 2637098947.
Projected, not verified: webpack-related CI runs ~1,000 h/week today; the faster Rspack compiles alone would save ~150 h/week, rising toward ~650 h/week once the webpack-only dev-server memory-test and bundle-size jobs are retired.
Dev-server start
| Metric | Webpack | Vite | Rspack |
|---|---|---|---|
| Cold start (first serve) | 174s | ~instant (on-demand) | 25s |
| HMR (1 component) | seconds | sub-second | sub-second |
Rspack and Vite both avoid Webpack's full upfront compile, so Rspack's dev start is Vite-comparable. Bundler-isolated; the shared ~2-min SCSS/Tailwind prelude (#604394) is excluded.
Initial-load JS requests per page
| Page | Webpack | Vite | Rspack |
|---|---|---|---|
| MR diffs | 21 | 305 | 24 |
| Repository | 19 | 195 | 9 |
| Boards | 10 | 171 | 7 |
| Dashboard | 10 | 143 | 9 |
Comparable total bytes per page; Rspack matches Webpack's request count, while Vite splits the same bytes into ~10–30× more files. Cold-load impact below.
Transport: cold-load cost of Vite's many chunks (HTTP/2)
Real headless Chrome over genuine HTTP/2 under GitLab's serving constraints (Cloudflare ~200-stream cap), cold cache, time-to-interactive (downloaded + executed), 9 runs/cell; plus a synthetic chunk-count sweep and the HTTP/2 literature.
On a cold first load, Vite is ~22–77% (~0.6–1.5 s) slower to interactive than Rspack at a typical 10 Mbps connection — across every page and latency tested.
| Condition (cold, time-to-interactive) | Vite vs Rspack |
|---|---|
| Fast broadband (~50 Mbps) | ~tie (≤120 ms) |
| Typical (~10 Mbps) | Vite +0.6–1.5 s (~22–77%) |
| Poor mobile (~2 Mbps) | Vite +2.4–4 s |
| Warm / repeat visit | gap erased |
| Post-deploy incremental | Vite ~10–25× better |
It is not the network: modulepreload flattens Vite's request waterfall to 2–3 rounds,
and the HTTP/2 stream cap never bites (Chrome spreads the requests across ~4 connections).
For the same total code, the slowdown is three costs that grow with the number of chunks:
- More bytes on the wire — gzip/brotli compress each file independently, so hundreds of tiny chunks compress worse than a few big ones; Vite ships ~1.1–1.3× more transferred bytes (+108% at 800 chunks), which directly costs download time on slow links.
- Per-request overhead — every request carries fixed header/dispatch cost, and hundreds of them add up where a handful of chunks pay almost nothing.
- Module execution — the browser parses and instantiates each module on the main thread; hundreds of small ES modules cost meaningfully more than a few chunks, adding a tail after download finishes (the dominant term on fast connections).
The synthetic sweep confirms it is chunk-count-driven: at a fixed 1.5 MB total, time is flat to ~50–100 chunks, then climbs steadily. GitLab's Vite pages emit ~140–305 requests (Rspack ~7–24) — well into the region where it hurts.
Caveats: imperceptible on fast/warm connections; Vite wins post-deploy incremental reuse; the poor-mobile figure is a conservative loss-free lower bound.
Risks
Vue 2 / Vue 3 "infection" (largest adaptation)
Selected apps run on Vue 3 (@vue/compat) via a ?vue3 query. Webpack propagates this
through its module graph at afterResolve; Rspack doesn't read beforeResolve mutations,
so we reproduce it with a loader plus a NormalModuleReplacementPlugin resolver
(vue3_infection_loader.js, vue3_infection_resolve_plugin.js). The subtle failure mode
is a silent reactivity split — a store lands on a second Vue instance and never updates,
with no console error — which a contributor can reintroduce unknowingly.
Mitigation: all 52 system pg17 shards green is the signal that catches a split (affected
specs render but never populate); ENABLE_RSPACK lets us diff Webpack vs Rspack on the
same commit. The mechanism self-deletes once the app is fully on Vue 3.
Plugin / compiler-internal gaps
~5–10% of webpack plugins reach into undocumented internals Rspack doesn't replicate (why
VueLoaderPlugin failed). Mitigation: the spike already exercised our full plugin set;
inventory the rest before the switch and replace any internal-API plugins with
Rspack-native equivalents.
GDK dev-server (nginx WebSocket upgrade)
The Rspack dev server needs nginx to proxy /_hmr/ as a WebSocket upgrade, upstreamed
into GDK's template (a hand-edit doesn't survive gdk reconfigure). Dev-only and off the
production-cutover path; it's prerequisite #604383 (closed).
