Skip to content

Commit b5b0e7b

Browse files
perf: shrink classic production browser output (#117)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 0963271 commit b5b0e7b

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'rsbuild-plugin-react-router': patch
3+
---
4+
5+
Shrink classic-mode production browser output. Production builds now mangle
6+
export names (`optimization.mangleExports: 'size'`), drop exports nothing
7+
imports across the whole graph (`optimization.usedExports: 'global'`), and name
8+
async chunks `static/js/async/[id]-[contenthash:16].js`. Classic mode resolves
9+
route modules through the browser manifest by chunk, so export names are not
10+
part of its runtime contract. RSC builds are unchanged and keep every export
11+
name because Flight resolves client references by name. Development builds are
12+
unchanged. To keep readable export names in a classic production build, set
13+
`optimization.mangleExports` and `optimization.usedExports` back in the
14+
function form of `tools.rspack`, which runs after plugin defaults are merged.

src/mode-plan.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,20 @@ const createClassicModePlan = async ({
345345
wasmLoading: 'fetch',
346346
library: { type: 'module' },
347347
module: true,
348+
// Async chunks are addressed by id at runtime, so the chunk name only
349+
// adds bytes to the filename and to every place that references it.
350+
...(isBuild
351+
? { chunkFilename: 'static/js/async/[id]-[contenthash:16].js' }
352+
: {}),
348353
},
349354
webOptimization: {
350355
avoidEntryIife: true,
351356
runtimeChunk: 'single',
357+
// Classic mode resolves route modules through the browser manifest by
358+
// chunk, not by export name, so production builds can mangle export
359+
// names and drop exports nothing imports across the whole graph. RSC
360+
// mode must keep every export name; see createRscModePlan.
361+
...(isBuild ? { mangleExports: 'size', usedExports: 'global' } : {}),
352362
},
353363
nodeExternals: Array.from(
354364
new Set(['express', ...getSsrExternals(process.cwd())])

tests/index.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,43 @@ describe('pluginReactRouter', () => {
445445
).toBeUndefined();
446446
});
447447

448+
it('shrinks classic production browser output', async () => {
449+
const rsbuild = await createStubRsbuild({
450+
action: 'build',
451+
rsbuildConfig: {},
452+
});
453+
454+
rsbuild.addPlugins([pluginReactRouter()]);
455+
const config = await rsbuild.unwrapConfig();
456+
457+
expect(config.environments.web.tools.rspack.optimization).toMatchObject({
458+
mangleExports: 'size',
459+
usedExports: 'global',
460+
});
461+
expect(config.environments.web.tools.rspack.output.chunkFilename).toBe(
462+
'static/js/async/[id]-[contenthash:16].js'
463+
);
464+
});
465+
466+
it('keeps classic development export names and chunk names', async () => {
467+
const rsbuild = await createStubRsbuild({
468+
rsbuildConfig: {},
469+
});
470+
471+
rsbuild.addPlugins([pluginReactRouter()]);
472+
const config = await rsbuild.unwrapConfig();
473+
474+
expect(
475+
config.environments.web.tools.rspack.optimization.mangleExports
476+
).toBeUndefined();
477+
expect(
478+
config.environments.web.tools.rspack.optimization.usedExports
479+
).toBeUndefined();
480+
expect(
481+
config.environments.web.tools.rspack.output.chunkFilename
482+
).toBeUndefined();
483+
});
484+
448485
it('preserves production export names for RSC builds', async () => {
449486
const rsbuild = await createStubRsbuild({
450487
action: 'build',

0 commit comments

Comments
 (0)