From d41472983074c0469060258376a897d9a1a3c8a8 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sat, 27 Jun 2026 05:13:22 +0000 Subject: [PATCH 1/3] experiment: run client route transforms as loader --- rslib.config.ts | 1 + src/build-output-transforms.ts | 21 ---------------- src/index.ts | 46 +++++++++++++++++++++++++++++++--- src/route-transform-loader.ts | 34 +++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 src/route-transform-loader.ts diff --git a/rslib.config.ts b/rslib.config.ts index 566bf25..18394a0 100644 --- a/rslib.config.ts +++ b/rslib.config.ts @@ -13,6 +13,7 @@ const config = defineConfig({ index: './src/index.ts', 'parallel-route-transform-worker': './src/parallel-route-transform-worker.ts', + 'route-transform-loader': './src/route-transform-loader.ts', 'templates/entry.server': './src/templates/entry.server.tsx', 'templates/entry.client': './src/templates/entry.client.tsx', }, diff --git a/src/build-output-transforms.ts b/src/build-output-transforms.ts index c06bae2..a2b011b 100644 --- a/src/build-output-transforms.ts +++ b/src/build-output-transforms.ts @@ -141,27 +141,6 @@ export const registerBuildOutputTransforms = ({ ) ); - api.transform( - { - resourceQuery: /__react-router-build-client-route/, - }, - async args => - performanceProfiler.record( - args.environment?.name, - 'route:client-entry', - args.resource, - async () => - routeTransformExecutor.run({ - kind: 'routeClientEntry', - code: args.code, - resourcePath: args.resourcePath, - environmentName: args.environment?.name, - isBuild, - routeChunkConfig, - }) - ) - ); - api.transform( { resourceQuery: /route-chunk=/, diff --git a/src/index.ts b/src/index.ts index 8dcbbbb..3a968a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { existsSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; import fsExtra from 'fs-extra'; import type { Config } from './react-router-config.js'; import type { RouteConfigEntry } from '@react-router/dev/routes'; @@ -52,7 +53,10 @@ import { type RouteChunkCache, type RouteChunkConfig, } from './route-chunks.js'; -import { createRouteTransformExecutor } from './parallel-route-transforms.js'; +import { + createRouteTransformExecutor, + getDefaultWorkerCount, +} from './parallel-route-transforms.js'; import { createRouteTopologyWatcher, createRouteManifestSnapshot, @@ -84,6 +88,10 @@ import { registerReactRouterTypegen } from './typegen.js'; export { loadReactRouterServerBuild } from './dev-generation.js'; export { resolveReactRouterServerBuild }; +const routeTransformLoaderPath = fileURLToPath( + new URL('./route-transform-loader.js', import.meta.url) +); + export const shouldParallelizeEnvironmentBuilds = ({ isBuild, spareCoreCount = getDefaultConcurrency(), @@ -412,8 +420,9 @@ export const pluginReactRouter = ( } const isBuild = api.context.action === 'build'; - const shouldDependOnWebCompiler = - !shouldParallelizeEnvironmentBuilds({ isBuild }); + const shouldDependOnWebCompiler = !shouldParallelizeEnvironmentBuilds({ + isBuild, + }); const isPrerenderEnabled = prerenderConfig !== undefined && prerenderConfig !== false; const isSpaMode = !ssr && !isPrerenderEnabled; @@ -429,6 +438,12 @@ export const pluginReactRouter = ( routeChunkCache, splitRouteModules: Boolean(splitRouteModules), }); + const routeLoaderWorkerCount = + pluginOptions.parallelTransforms === false + ? 0 + : typeof pluginOptions.parallelTransforms === 'number' + ? pluginOptions.parallelTransforms + : getDefaultWorkerCount(); const routeChunkOptions = { splitRouteModules, rootRouteFile, @@ -929,6 +944,31 @@ export const pluginReactRouter = ( ensureFederationAsyncStartup(rspackConfig); } + rspackConfig.module = { + ...rspackConfig.module, + rules: [ + ...(rspackConfig.module?.rules ?? []), + { + resourceQuery: /__react-router-build-client-route/, + use: [ + { + loader: routeTransformLoaderPath, + options: { + kind: 'routeClientEntry', + environmentName: name, + isBuild, + routeChunkConfig, + }, + parallel: + routeLoaderWorkerCount > 0 + ? { maxWorkers: routeLoaderWorkerCount } + : false, + }, + ], + }, + ], + }; + if (name === 'node') { const output = rspackConfig.output; if (output) { diff --git a/src/route-transform-loader.ts b/src/route-transform-loader.ts new file mode 100644 index 0000000..95e3cf0 --- /dev/null +++ b/src/route-transform-loader.ts @@ -0,0 +1,34 @@ +import type { LoaderDefinition } from '@rspack/core'; +import { executeRouteTransformTask } from './route-transform-tasks.js'; +import type { RouteChunkConfig } from './route-chunks.js'; + +type RouteTransformLoaderOptions = { + kind: 'routeClientEntry'; + environmentName: string; + isBuild: boolean; + routeChunkConfig: RouteChunkConfig; +}; + +const routeTransformLoader: LoaderDefinition = + function routeTransformLoader(code) { + const callback = this.async(); + const options = this.getOptions(); + + executeRouteTransformTask( + { + kind: options.kind, + code, + resourcePath: this.resourcePath, + environmentName: options.environmentName, + isBuild: options.isBuild, + routeChunkConfig: options.routeChunkConfig, + }, + {} + ).then( + result => callback(null, result.code, result.map ?? undefined), + error => + callback(error instanceof Error ? error : new Error(String(error))) + ); + }; + +export default routeTransformLoader; From 351c08cb0bdb1d1f37130ff7eab609b2b7f7e44a Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sat, 27 Jun 2026 05:29:37 +0000 Subject: [PATCH 2/3] Revert "experiment: run client route transforms as loader" This reverts commit d41472983074c0469060258376a897d9a1a3c8a8. --- rslib.config.ts | 1 - src/build-output-transforms.ts | 21 ++++++++++++++++ src/index.ts | 46 +++------------------------------- src/route-transform-loader.ts | 34 ------------------------- 4 files changed, 24 insertions(+), 78 deletions(-) delete mode 100644 src/route-transform-loader.ts diff --git a/rslib.config.ts b/rslib.config.ts index 18394a0..566bf25 100644 --- a/rslib.config.ts +++ b/rslib.config.ts @@ -13,7 +13,6 @@ const config = defineConfig({ index: './src/index.ts', 'parallel-route-transform-worker': './src/parallel-route-transform-worker.ts', - 'route-transform-loader': './src/route-transform-loader.ts', 'templates/entry.server': './src/templates/entry.server.tsx', 'templates/entry.client': './src/templates/entry.client.tsx', }, diff --git a/src/build-output-transforms.ts b/src/build-output-transforms.ts index a2b011b..c06bae2 100644 --- a/src/build-output-transforms.ts +++ b/src/build-output-transforms.ts @@ -141,6 +141,27 @@ export const registerBuildOutputTransforms = ({ ) ); + api.transform( + { + resourceQuery: /__react-router-build-client-route/, + }, + async args => + performanceProfiler.record( + args.environment?.name, + 'route:client-entry', + args.resource, + async () => + routeTransformExecutor.run({ + kind: 'routeClientEntry', + code: args.code, + resourcePath: args.resourcePath, + environmentName: args.environment?.name, + isBuild, + routeChunkConfig, + }) + ) + ); + api.transform( { resourceQuery: /route-chunk=/, diff --git a/src/index.ts b/src/index.ts index 3a968a1..8dcbbbb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import { existsSync, readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; import fsExtra from 'fs-extra'; import type { Config } from './react-router-config.js'; import type { RouteConfigEntry } from '@react-router/dev/routes'; @@ -53,10 +52,7 @@ import { type RouteChunkCache, type RouteChunkConfig, } from './route-chunks.js'; -import { - createRouteTransformExecutor, - getDefaultWorkerCount, -} from './parallel-route-transforms.js'; +import { createRouteTransformExecutor } from './parallel-route-transforms.js'; import { createRouteTopologyWatcher, createRouteManifestSnapshot, @@ -88,10 +84,6 @@ import { registerReactRouterTypegen } from './typegen.js'; export { loadReactRouterServerBuild } from './dev-generation.js'; export { resolveReactRouterServerBuild }; -const routeTransformLoaderPath = fileURLToPath( - new URL('./route-transform-loader.js', import.meta.url) -); - export const shouldParallelizeEnvironmentBuilds = ({ isBuild, spareCoreCount = getDefaultConcurrency(), @@ -420,9 +412,8 @@ export const pluginReactRouter = ( } const isBuild = api.context.action === 'build'; - const shouldDependOnWebCompiler = !shouldParallelizeEnvironmentBuilds({ - isBuild, - }); + const shouldDependOnWebCompiler = + !shouldParallelizeEnvironmentBuilds({ isBuild }); const isPrerenderEnabled = prerenderConfig !== undefined && prerenderConfig !== false; const isSpaMode = !ssr && !isPrerenderEnabled; @@ -438,12 +429,6 @@ export const pluginReactRouter = ( routeChunkCache, splitRouteModules: Boolean(splitRouteModules), }); - const routeLoaderWorkerCount = - pluginOptions.parallelTransforms === false - ? 0 - : typeof pluginOptions.parallelTransforms === 'number' - ? pluginOptions.parallelTransforms - : getDefaultWorkerCount(); const routeChunkOptions = { splitRouteModules, rootRouteFile, @@ -944,31 +929,6 @@ export const pluginReactRouter = ( ensureFederationAsyncStartup(rspackConfig); } - rspackConfig.module = { - ...rspackConfig.module, - rules: [ - ...(rspackConfig.module?.rules ?? []), - { - resourceQuery: /__react-router-build-client-route/, - use: [ - { - loader: routeTransformLoaderPath, - options: { - kind: 'routeClientEntry', - environmentName: name, - isBuild, - routeChunkConfig, - }, - parallel: - routeLoaderWorkerCount > 0 - ? { maxWorkers: routeLoaderWorkerCount } - : false, - }, - ], - }, - ], - }; - if (name === 'node') { const output = rspackConfig.output; if (output) { diff --git a/src/route-transform-loader.ts b/src/route-transform-loader.ts deleted file mode 100644 index 95e3cf0..0000000 --- a/src/route-transform-loader.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { LoaderDefinition } from '@rspack/core'; -import { executeRouteTransformTask } from './route-transform-tasks.js'; -import type { RouteChunkConfig } from './route-chunks.js'; - -type RouteTransformLoaderOptions = { - kind: 'routeClientEntry'; - environmentName: string; - isBuild: boolean; - routeChunkConfig: RouteChunkConfig; -}; - -const routeTransformLoader: LoaderDefinition = - function routeTransformLoader(code) { - const callback = this.async(); - const options = this.getOptions(); - - executeRouteTransformTask( - { - kind: options.kind, - code, - resourcePath: this.resourcePath, - environmentName: options.environmentName, - isBuild: options.isBuild, - routeChunkConfig: options.routeChunkConfig, - }, - {} - ).then( - result => callback(null, result.code, result.map ?? undefined), - error => - callback(error instanceof Error ? error : new Error(String(error))) - ); - }; - -export default routeTransformLoader; From ba9023e8208cf25761ca18b915e7c8ed79a44feb Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sat, 27 Jun 2026 05:30:32 +0000 Subject: [PATCH 3/3] docs: record route loader benchmark result --- src/build-output-transforms.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/build-output-transforms.ts b/src/build-output-transforms.ts index c06bae2..93be149 100644 --- a/src/build-output-transforms.ts +++ b/src/build-output-transforms.ts @@ -141,6 +141,12 @@ export const registerBuildOutputTransforms = ({ ) ); + // Rspack loader rules support native worker parallelism via + // `use: [{ loader, options, parallel: { maxWorkers } }]`. We tested moving + // these pure route transforms to that shape, but the large dev benchmark was + // slower overall: server-ready improved slightly while lazy route requests + // regressed. Keep the custom executor until loader workers can share the + // route transform cache and avoid the lazy-route penalty. api.transform( { resourceQuery: /__react-router-build-client-route/,