Bundle-size testbed comparing Vite, Webpack (swc-loader), and Rspack (builtin:swc-loader) against the same React 19 + @fluentui/react-components v9 source. Produces a unified per-chunk table and verifies that tabster never lands in a main chunk.
apps/ # test apps (one folder each)
menu-app/ # Button + Input eager; Menu lazy (pulls in tabster)
button-app/ # Button + Input eager; Button-only lazy (no Menu, no tabster)
multi-lazy-app/ # five sibling lazy chunks: Menu, Popover, Dialog, Tab, Combobox
tabster-split-app/ # two lazy chunks: Modalizer (Dialog) + Mover (TabList)
configs/ # one bundler config per file, all factories
vite.config.ts
webpack.config.ts
rspack.config.ts
scripts/
build.ts # dispatcher — `pnpm build:<bundler> <app>`
report.ts # size reporter — `pnpm report <app>`
dist/<bundler>/<app>/ # build output
pnpm install
pnpm build:vite menu-app # one bundler, one app
pnpm build:webpack menu-app
pnpm build:rspack menu-app
pnpm build menu-app # all three bundlers + report
pnpm report menu-app # just the report (assumes builds exist)Apps supported out of the box: menu-app, button-app, multi-lazy-app, tabster-split-app. Adding a new app is one folder under apps/<name>/ (with package.json, index.html, main.tsx, App.tsx, and lazy modules as desired).
App: menu-app
Description: Fluent Button + Input eagerly; Menu tree (MenuTrigger/MenuPopover/MenuList/MenuItem) lazy.
bundler chunk role raw gzip tabster
------- ---------------------------- ---- ------- ------ -----------
vite assets/LazyMenu-CStpSPMr.js lazy 142.0KB 44.2KB yes(59.9KB)
vite assets/index-DlndUfKc.js main 273.9KB 82.1KB no
webpack 151.9f2d200a21b407a80798.js lazy 144.3KB 43.7KB yes(60.3KB)
webpack 740.dc62c6ab880818492e20.js lazy 555B 344B no
webpack main.05819c118c4047448637.js main 301.9KB 86.7KB no
rspack 43.b0c98844397b3ca2.js lazy 139.4KB 43.2KB yes(57.8KB)
rspack 894.c1f1dbeb44ed35a4.js lazy 552B 340B no
rspack main.4dc770848e14466f.js main 301.1KB 86.6KB no
Totals (raw): vite 415.9KB | webpack 446.8KB | rspack 441.0KB
tabster check: PASS — no main chunk contains tabster
All three bundlers produce production builds with source maps enabled. The reporter:
- Reads
apps/<app>/package.jsonfor the human-readable description - Parses each bundler's emitted
index.htmlto identify the main entry chunk; classifies the rest as lazy - Measures raw and gzip size per chunk
- Runs
source-map-exploreragainst each chunk to attribute bytes to npm packages - Fails with exit 1 if any main chunk contains
tabsterbytes
Default code splitting is used throughout — no splitChunks/manualChunks customization. The goal is to measure what each bundler does out of the box.
- Single pnpm project, not a monorepo. One dependency tree so all three bundlers see identical package versions.
- Programmatic builds, no CLIs.
scripts/build.tscallsvite.build(),webpack(), andrspack()directly. Avoids ts-node / loader pluggability issues. - No custom minification. Each bundler uses its default (esbuild for Vite, Terser for Webpack, Rspack's built-in for Rspack). Size differences partially reflect minifier choice — that's the point.
- No CSS handling. Fluent v9 uses Griffel (runtime CSS-in-JS); no static CSS is emitted.
The repo ships a custom build of tabster that splits each get* function (getMover, getModalizer, getGroupper, getDeloser, getRestorer, getObservedElement, getOutline, getCrossOrigin) into its own module and exposes them as subpath exports (tabster/mover, tabster/modalizer, …). A pnpm patch against @fluentui/react-tabster rewrites its hook imports to use those subpaths.
Together this lets bundlers isolate tabster subsystems per lazy chunk: an app that only uses Dialog no longer pulls in Mover/Groupper/CrossOrigin code, and vice versa. See tabster-split-app for a before/after demonstration.
Vendored tgz: vendor/tabster-8.7.0.tgz. Patch: patches/@fluentui__react-tabster.patch. To rebuild the tabster tgz from source, clone microsoft/tabster into tabster/ and follow the instructions in that directory (the refactored source lives under gitignored tabster/ on the author's machine).
Full design and implementation history:
docs/superpowers/specs/2026-04-21-bundle-size-testbed-design.mddocs/superpowers/plans/2026-04-21-bundle-size-testbed.md