Skip to content

Repository files navigation

testing-fluent

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.

Layout

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

Commands

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).

Example output

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

How it works

All three bundlers produce production builds with source maps enabled. The reporter:

  1. Reads apps/<app>/package.json for the human-readable description
  2. Parses each bundler's emitted index.html to identify the main entry chunk; classifies the rest as lazy
  3. Measures raw and gzip size per chunk
  4. Runs source-map-explorer against each chunk to attribute bytes to npm packages
  5. Fails with exit 1 if any main chunk contains tabster bytes

Default code splitting is used throughout — no splitChunks/manualChunks customization. The goal is to measure what each bundler does out of the box.

Design decisions

  • Single pnpm project, not a monorepo. One dependency tree so all three bundlers see identical package versions.
  • Programmatic builds, no CLIs. scripts/build.ts calls vite.build(), webpack(), and rspack() 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.

Forked tabster + patched react-tabster

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).

Specs & plans

Full design and implementation history:

  • docs/superpowers/specs/2026-04-21-bundle-size-testbed-design.md
  • docs/superpowers/plans/2026-04-21-bundle-size-testbed.md

About

Bundle-size testbed comparing Vite, Webpack (swc-loader), and Rspack against React 19 + Fluent UI v9

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages