Request
Please add an opt-in development mode that keeps React Router server entries,
loaders and SSR working while compiling server-side dynamic imports only when
they are first requested. It should preserve the in-flight SSR request across
that compilation and retain normal HMR and hot data revalidation for actual
source edits.
This is a feature request, not a claim that an existing SSR-lazy option is
broken. With Rsbuild 2.2.9 and rsbuild-plugin-react-router 0.8.1,
dev.lazyCompilation is applied only to target: 'web'. The server
compiler still traverses dynamic imports on a cold build even when no request
will use them. Large SSR applications with many optional React.lazy
components therefore pay for both browser startup and eager server compilation.
It differs from skipping server reevaluation when output is unchanged: the
first use of a deferred server import deliberately changes the Node module
graph and must resolve a Promise that is already held by a live SSR request.
Public demonstration for evaluating the feature
The following graph can be added to a standard SSR example using
pluginReactRouter() and pluginReact(), with ssr: true and
dev.lazyCompilation: { entries: false, imports: true }. It is proposed
test input, not a benchmark that has already been run.
Create generate-optional.mjs in the example root:
import { mkdir, writeFile } from 'node:fs/promises';
const count = Number(process.argv[2] ?? 1000);
await mkdir('app/generated', { recursive: true });
await Promise.all(Array.from({ length: count }, (_, i) =>
writeFile(`app/generated/optional-${i}.tsx`,
`export default function Optional() {
return <p className="optional">Optional item ${i}</p>;
}\n`)
));
await writeFile('app/generated/registry.tsx',
"import { lazy } from 'react';\nexport const optional = [\n" +
Array.from({ length: count }, (_, i) =>
` lazy(() => import('./optional-${i}')),`).join('\n') +
'\n];\n');
Create app/routes/catalog.tsx:
import { Suspense } from 'react';
import { useLoaderData, type LoaderFunctionArgs } from 'react-router';
import { optional } from '../generated/registry';
import './catalog.css';
export function loader({ params }: LoaderFunctionArgs) {
return { item: Number(params.item ?? 0) };
}
export default function Catalog() {
const { item } = useLoaderData<typeof loader>();
const Selected = optional[item];
return <Suspense fallback={<p>Loading item</p>}><Selected /></Suspense>;
}
Create app/routes/catalog.css:
.optional { color: darkslateblue; }
Add route("catalog/:item", "routes/catalog.tsx") to the example's
app/routes.ts, alongside an ordinary index route with a text field. Run:
node generate-optional.mjs 1000
rsbuild dev
For a cold comparison, use a new persistent-cache directory for each run.
Measure time from process launch to the first index page with the text field
usable, and also count compiled Node modules under app/generated/. Then
open /catalog/17 in a second tab. Record time to first SSR content and to
interactivity for that request separately. Repeat at 100 and 1,000 leaves to
see how cold startup scales; do not add startup and request timings together
or count a test timeout as bundler work.
Current source predicts all referenced Node leaves enter the first build.
The desired implementation should build only the required SSR shell at cold
startup, compile item 17 on the request that needs it, and leave unused items
deferred. The first tab should not reload or lose its text merely because the
second tab activates a server import.
Required ownership and correctness
- Keep the React Router server entry eager. Deferring it can deadlock
createDevServer() before the lazy compilation middleware is listening.
- The Router runtime needs to distinguish an actual Node lazy activation from
an unknown invalidation. An empty source-file list alone is insufficient:
build errors, a superseded generation or failed Web compilation must not be
accepted as a valid server update.
- Resume the suspended SSR import only after the corresponding Node update
commits against a successful, settled Web manifest. On failure or shutdown,
reject its waiters; avoid hanging requests or exposing a half-applied runtime.
- Actual loader/action/dependency edits must still run both required compilers
and revalidate data. CSS edits/imports and dynamically visited routes must
retain the correct stylesheet and HMR behavior.
- If Node evaluation calls a dynamic import before HTTP listen, support a
safe in-process activation path, or reject an unsupported configuration
clearly. Awaiting the HTTP lazy endpoint before it exists is not sufficient.
Suggested implementation boundary
Rspack already exposes lazyCompilation for a Node compiler and supplies a
Node lazy client; Rsbuild currently installs the lazy middleware for any
compiler with that option. The missing part is ownership in the Router server
runtime: retained Node evaluation/HMR, safe lazy-activation attribution, and
settling the pending import with the paired Web generation. The existing
last-good-build contract should continue to protect failed builds.
An opt-in option is appropriate until these cases work in both ESM and CJS
plugin consumption. If Rspack does not expose enough structured information to
attribute activations or delay their resolution, please request a small typed
hook there rather than using log-message matching.
Scope and verification
This request is based on the public compiler configuration and server-lifecycle behavior described above. The example graph is proposed test input, not a completed stock/candidate benchmark. No numeric performance improvement is claimed here. Please measure with stock dependencies and the same generated input before and after an implementation.
This differs from #140, which avoids reevaluating already built, unchanged Node output. Both features may be useful, but one does not implement the other.
Request
Please add an opt-in development mode that keeps React Router server entries,
loaders and SSR working while compiling server-side dynamic imports only when
they are first requested. It should preserve the in-flight SSR request across
that compilation and retain normal HMR and hot data revalidation for actual
source edits.
This is a feature request, not a claim that an existing SSR-lazy option is
broken. With Rsbuild 2.2.9 and rsbuild-plugin-react-router 0.8.1,
dev.lazyCompilationis applied only totarget: 'web'. The servercompiler still traverses dynamic imports on a cold build even when no request
will use them. Large SSR applications with many optional
React.lazycomponents therefore pay for both browser startup and eager server compilation.
It differs from skipping server reevaluation when output is unchanged: the
first use of a deferred server import deliberately changes the Node module
graph and must resolve a Promise that is already held by a live SSR request.
Public demonstration for evaluating the feature
The following graph can be added to a standard SSR example using
pluginReactRouter()andpluginReact(), withssr: trueanddev.lazyCompilation: { entries: false, imports: true }. It is proposedtest input, not a benchmark that has already been run.
Create
generate-optional.mjsin the example root:Create
app/routes/catalog.tsx:Create
app/routes/catalog.css:Add
route("catalog/:item", "routes/catalog.tsx")to the example'sapp/routes.ts, alongside an ordinary index route with a text field. Run:For a cold comparison, use a new persistent-cache directory for each run.
Measure time from process launch to the first index page with the text field
usable, and also count compiled Node modules under
app/generated/. Thenopen
/catalog/17in a second tab. Record time to first SSR content and tointeractivity for that request separately. Repeat at 100 and 1,000 leaves to
see how cold startup scales; do not add startup and request timings together
or count a test timeout as bundler work.
Current source predicts all referenced Node leaves enter the first build.
The desired implementation should build only the required SSR shell at cold
startup, compile item 17 on the request that needs it, and leave unused items
deferred. The first tab should not reload or lose its text merely because the
second tab activates a server import.
Required ownership and correctness
createDevServer()before the lazy compilation middleware is listening.an unknown invalidation. An empty source-file list alone is insufficient:
build errors, a superseded generation or failed Web compilation must not be
accepted as a valid server update.
commits against a successful, settled Web manifest. On failure or shutdown,
reject its waiters; avoid hanging requests or exposing a half-applied runtime.
and revalidate data. CSS edits/imports and dynamically visited routes must
retain the correct stylesheet and HMR behavior.
safe in-process activation path, or reject an unsupported configuration
clearly. Awaiting the HTTP lazy endpoint before it exists is not sufficient.
Suggested implementation boundary
Rspack already exposes
lazyCompilationfor a Node compiler and supplies aNode lazy client; Rsbuild currently installs the lazy middleware for any
compiler with that option. The missing part is ownership in the Router server
runtime: retained Node evaluation/HMR, safe lazy-activation attribution, and
settling the pending import with the paired Web generation. The existing
last-good-build contract should continue to protect failed builds.
An opt-in option is appropriate until these cases work in both ESM and CJS
plugin consumption. If Rspack does not expose enough structured information to
attribute activations or delay their resolution, please request a small typed
hook there rather than using log-message matching.
Scope and verification
This request is based on the public compiler configuration and server-lifecycle behavior described above. The example graph is proposed test input, not a completed stock/candidate benchmark. No numeric performance improvement is claimed here. Please measure with stock dependencies and the same generated input before and after an implementation.
This differs from #140, which avoids reevaluating already built, unchanged Node output. Both features may be useful, but one does not implement the other.