Adopt Rspack: proposal & rollout plan

image

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:

  1. GDK dev-server integration — upstream the HMR WebSocket-upgrade fix into GDK's nginx template so it survives gdk reconfigure. Prereq for #604385.
  2. Switch to Rspack — enable Rspack for dev + CI and disable Vite, in one step. Depends on #604383 (closed).
  3. Remove Webpack — delete config, deps, and CI jobs once the switch is proven. Depends on #604385.
  4. 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:

  1. 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.
  2. Per-request overhead — every request carries fixed header/dispatch cost, and hundreds of them add up where a handful of chunks pay almost nothing.
  3. 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).

Edited by 🤖 GitLab Bot 🤖