Skip to content

build(frontend): upgrade rsbuild to 2.2.0 - #2613

Open
malinskibeniamin wants to merge 2 commits into
masterfrom
ben-malinski/rsbuild-2-2-beta-upgrade
Open

build(frontend): upgrade rsbuild to 2.2.0#2613
malinskibeniamin wants to merge 2 commits into
masterfrom
ben-malinski/rsbuild-2-2-beta-upgrade

Conversation

@malinskibeniamin

@malinskibeniamin malinskibeniamin commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Proven impact

Metric Before After Delta
Largest emitted JavaScript asset 10.28 MB 5.95 MB -4.33 MB (-42.1%)
Median production build CPU time 35.79 s 28.65 s -7.14 s (-20.0%)
Total emitted JavaScript 43.19 MB 43.10 MB -0.09 MB (-0.2%)

Value proven: The upgraded compiler pipeline uses less build CPU and separates the former oversized mixed vendor chunk without increasing total JavaScript output.

Method: five bun run build-local-test runs each for exact merge-base d146e9809 and candidate b5e10815c on the same Apple Silicon development machine with fixed build environment values. Bundle measurements use emitted production assets. Against the pre-experiment PR commit, experiment opt-ins changed median CPU by +1.2%, within run noise, and left emitted JavaScript size unchanged; no separate experiment-only performance gain is claimed.

Summary

  • Upgrade Rsbuild to the 2.2.0 GA release and update every official Rsbuild plugin to its latest compatible release.
  • Replace the Babel React Compiler transform with Rsbuild's native Rust implementation and remove the React 19-unneeded compiler runtime.
  • Enable every generally applicable Rspack experiment: async WebAssembly, future defaults, native watcher, production pure-function analysis, and Rsbuild-provided source imports.
  • Keep feature-specific or incompatible experiments off: remote HTTP builds, unused deferred-import syntax, custom filesystem routing, and the unfinished ESM-only Rspack runtime mode.
  • Remove fasterModuleConcatenation, which Rspack removed before GA after reverting the underlying optimization.
  • Parallelize SVGR and split asynchronous legacy UI and Monaco vendors into reusable chunks with a 512 KiB async target.
  • Add a production-config contract test for compiler, experiment, and chunking behavior.

Why

Adopt Rsbuild 2.2's GA compiler, watcher, forward defaults, and bundling improvements while reducing build overhead and the worst emitted chunk size. Application behavior and backend integrations are out of scope.

Surface review skipped: build-only change; no rendered UI changed.

Experiment scope

  • asyncWebAssembly: true: current Rspack default, pinned explicitly for the supported async Wasm pipeline.
  • futureDefaults: true: opts into the next-major defaults now so incompatibilities fail during deliberate upgrades.
  • nativeWatcher: true: uses Rust file watching for development rebuilds.
  • pureFunctions: isProd: enables cross-module pure-function analysis only where production tree shaking applies.
  • sourceImport: true: enabled by Rsbuild 2.2 and protected by the config contract.
  • Excluded buildHttp because Console has no remote URL imports and enabling network-backed builds would expand the supply-chain surface.
  • Excluded deferImport because Console uses no deferred-import proposal syntax.
  • Excluded runtimeMode: 'rspack' because it is still under development and targets ESM modern-module output, while Console uses Module Federation's current runtime.
  • Excluded useInputFileSystem because Console does not replace Rspack's input filesystem.

Commits

  • 8df6382bb build(frontend): upgrade rsbuild to 2.2
  • b5e10815c build(frontend): enable rspack experiments

Reviewer guide

Start with frontend/rsbuild.config.ts, then review dependency intent in frontend/package.json, the contract in frontend/tests/rsbuild-config.test.ts, and finally the generated lockfiles.

Dogfood evidence

  • Verdict: PASS
  • Entrypoint: bun run start -- --port 57086, bun x rsbuild preview --port 57087, browser routes, and emitted asset/federation HTTP endpoints.
  • Actions and break attempts: Loaded development and production builds; triggered native-watcher HMR; navigated and reloaded /overview and /topics; opened an invalid deep link; exercised missing-backend states; fetched large vendor chunks, workers, and Module Federation artifacts.
  • Observations: HMR rebuilt in 1.25 s without losing page state. Production routes and deep-link reloads rendered. The invalid route showed its not-found recovery. Federation manifests were valid JSON; workers, Monaco, legacy UI chunks, and manifests returned HTTP 200 at their exact emitted sizes.
  • Repairs and replay: A RED config contract proved the new experiment policy before implementation. The production build and all entrypoints replayed successfully after enabling it.
  • Limits: No live Redpanda backend, so live cluster data workflows were not exercised; expected proxy 504s rendered existing unavailable states.

Dependency upgrade path

  • Upgrade evidence: Production config contracts were developed RED → GREEN; frozen install, type checking, build, full tests, benchmark, and browser dogfood passed.
  • Packages: @rsbuild/core@2.2.0; React 2.1.0; Sass 2.0.1; SVGR 2.0.5; Tailwind CSS 2.0.3; YAML 2.0.0; Node polyfill remains latest at 1.4.6. Removed @rsbuild/plugin-babel, babel-plugin-react-compiler, and react-compiler-runtime.
  • SemVer confidence: Core is exactly pinned to GA 2.2.0; the lockfiles resolve @rspack/core@2.2.0. Official plugin upgrades use their current releases.
  • Risk gate: Draft PR; exact core pinning contains experimental API drift, the config contract protects adopted APIs, and development plus production entrypoints were exercised.

Test plan

  • bun install --frozen-lockfile --ignore-scripts
  • bun run type:check
  • bun run lint:check:file rsbuild.config.ts tests/rsbuild-config.test.ts
  • Rsbuild config contract test
  • Unit suite: 838 tests
  • Integration suite: 1,256 tests
  • Five merge-base and five candidate bun run build-local-test benchmark runs
  • Development HMR and production browser dogfood

@malinskibeniamin malinskibeniamin self-assigned this Aug 24, 2026

@malinskibeniamin malinskibeniamin left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated /review: 9 finding(s).

These findings could not be anchored to a changed line:

  • P1 frontend/bun.lock:400Duplicate @rspack/core instance for module federation.

@rsbuild/core@2.2.0-beta.2 resolves @rspack/core@2.2.0-rc.0, but this lock now nests a second copy: @module-federation/rspack/@rspack/core → 2.0.6 (and the same for @rsdoctor/rspack-plugin / @rsdoctor/types). Before this change everything deduped on 2.0.6.

@module-federation/rspack imports @rspack/core directly, so its plugin classes and hook taps come from a different rspack instance — and a different native binding — than the compiler that runs them. Console ships embedded.js as a federated remote consumed by Cloud UI, so this sits on the critical output path, and it also pulls a second rspack native binary into the install.

Correction: align the versions — either hold @rsbuild/core on the 2.0.x line until @module-federation/rsbuild-plugin supports rspack 2.2, or add an override so a single @rspack/core resolves for all consumers.

Verify: cd frontend && bun install && bun pm ls | grep rspack (expect exactly one @rspack/core and one @rspack/binding), then bun run build and load embedded.js from a federation host.

  • P2 frontend/package.json:788A beta build toolchain is pinned for release builds, with a floating prerelease range underneath.

@rsbuild/core: 2.2.0-beta.2 declares @rspack/core: ~2.2.0-beta.1, which the lock satisfied with @rspack/core@2.2.0-rc.0 — a different prerelease than the one named. That range keeps floating across prereleases (beta.3, rc.1, …) on any lock refresh, so the bundler that produces the shipped artifact can change without a reviewed diff, and prereleases carry no compatibility guarantee.

Correction: if the 2.2 features are needed now, state that in the commit body and add an exact override for @rspack/core so the prerelease cannot drift; otherwise wait for @rsbuild/core@2.2.0 stable.

Verify: cd frontend && bun install --frozen-lockfile && bun pm ls | grep '@rspack/core' — the resolved version should equal the pinned one on a clean install.

reuseExistingChunk: true,
},
monaco: {
test: /[\\/]node_modules[\\/]monaco-editor[\\/]/,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority: P1

The Monaco enforce cache group can break Monaco's web workers.

MonacoWebpackPlugin (configured at line 193 with filename: 'static/js/[name].worker.js') emits separate entrypoints whose modules come from node_modules/monaco-editor. This cache group's test matches exactly those modules, and with the new top-level chunks: 'all' plus enforce: true, the worker entrypoints' own code becomes eligible to be hoisted into the shared lib-monaco-editor chunk.

A worker entry that depends on a second emitted file has no way to load it — the plugin points the worker URL at a single *.worker.js. The consequence is that yaml/json/typescript/protobuf language services fail at runtime (validation, completion, hover) while the editor itself still renders, so this fails quietly.

Correction: scope the cache group away from worker entries, e.g. add chunks: (chunk) => !chunk.name?.endsWith('worker') to the monaco group (and confirm the same for legacyUi), or drop enforce so size heuristics do not force the extraction.

Verify: cd frontend && bun run build, then check each build/static/js/*.worker.js is self-contained (no __webpack_require__.e / chunk-load calls) and open a topic's YAML/JSON editor in the built app with the console open — no worker load errors.

Automated /review.

Comment thread frontend/rsbuild.config.ts Outdated
config.experiments = {
...config.experiments,
lazyBarrel: false,
...(isProd ? { fasterModuleConcatenation: true } : {}),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority: P2

lazyBarrel: false was dropped here without justification.

This line replaces the previous explicit lazyBarrel: false in config.experiments. Removing the opt-out re-enables Rspack's barrel-file optimization, which changes when re-export barrels' modules are evaluated and therefore the ordering of import side effects. This config depends on side-effect-sensitive setup (Monaco worker registration, the react-onclickoutside and date-fns-tz shims aliased just below, CSS imports), and the commit is titled purely as a version upgrade — so the behavior change is invisible to anyone reading the log.

Correction: restore lazyBarrel: false, or keep it removed and record in the commit body why the flag was originally set and what evidence shows enabling it is safe now.

Verify: cd frontend && bun run build and compare the emitted chunk graph against master, then smoke-test the app for missing side effects (Monaco workers, date formatting, datepicker shim).

Automated /review.


import { fileURLToPath } from 'node:url';

vi.mock('@module-federation/rsbuild-plugin', () => ({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority: P2

Mocking pluginModuleFederation means this test does not describe the production config.

The real Module Federation plugin mutates optimization (including splitChunks) and injects the container/remote entries. Stubbing it out to a no-op setup means the asserted optimization.splitChunks shape and the serializedConfig string checks below are taken from a pipeline that never ships — the test can stay green while the actual production config differs on exactly the surface this PR changes.

Correction: either drop the mock and let the real plugin run during inspectConfig (it needs no network), or narrow the test's claims to the values this config file sets directly and assert the federation-influenced shape in the integration/build suite instead.

Verify: remove the vi.mock block and re-run the file; if the assertions still hold, the mock was unnecessary — if they change, the mock was hiding the real config.

Automated /review.

fasterModuleConcatenation: true,
nativeWatcher: true,
});
expect(rspackConfig?.experiments).not.toHaveProperty('lazyBarrel');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority: P3

not.toHaveProperty('lazyBarrel') is a change detector, not a behavior assertion.

It asserts the absence of an upstream default rather than anything about the build output, so it will fail whenever Rspack begins materializing lazyBarrel in the resolved experiments — an unrelated upgrade breaks the test with no product defect. Prefer asserting the properties this config intentionally sets and leave upstream defaults unconstrained.

Automated /review.

import rsbuildConfig from '../rsbuild.config';

describe('Rsbuild production config', () => {
it('uses the optimized compiler and chunking pipeline', async () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority: P3

Repo convention is test() rather than it() for test cases (see the project test guidance and neighbouring files under frontend/tests/). Rename for consistency.

Automated /review.


describe('Rsbuild production config', () => {
it('uses the optimized compiler and chunking pipeline', async () => {
const rsbuild = await createRsbuild({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority: P3

A full createRsbuild() + inspectConfig() run now lives in the fast unit project.

vitest.config.unit.mts includes tests/**/*.test.ts with environment: 'node', so this case joins the pure unit suite while depending on loadEnv, plugin resolution, the TanStack Router plugin, and the real filesystem/cwd. That makes the unit suite slower and environment-sensitive.

Correction: move this to the integration config (or a dedicated build-config project) so unit runs stay hermetic and quick.

Verify: cd frontend && bun run test:unit and compare suite duration against master.

Automated /review.

preset: 'default',
chunks: 'all',
// Cap asynchronous chunks without imposing a size limit on initial chunks.
maxAsyncSize: 512 * 1024,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority: P3

maxAsyncSize: 512 * 1024 has no supporting measurement. The comment explains what the cap does but not why 512 KB is the right threshold — capping every async chunk trades request count against payload size, and the effect depends on this app's route-split shape. Recording the before/after bundle report in the commit body (the config already supports RSDOCTOR=1) would make the number reviewable and safe to revisit later.

Automated /review.

@malinskibeniamin
malinskibeniamin requested review from a team, Mateoc, SpicyPete, jvorcak and sago2k8 and removed request for a team August 24, 2026 13:18
@malinskibeniamin
malinskibeniamin force-pushed the ben-malinski/rsbuild-2-2-beta-upgrade branch from f20a76e to 8df6382 Compare August 26, 2026 22:36
@malinskibeniamin malinskibeniamin changed the title build(frontend): upgrade rsbuild to 2.2.0-beta.2 build(frontend): upgrade rsbuild to 2.2.0 Aug 26, 2026
@malinskibeniamin
malinskibeniamin marked this pull request as ready for review August 27, 2026 12:36
@malinskibeniamin
malinskibeniamin enabled auto-merge (squash) August 27, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant