Skip to content

[Perf]: reuse unchanged server modules through Rsbuild without skipping loader refresh #140

Description

@matthewdavis-oai

Problem and requested behavior

A client component edit can change only a Node chunk that the server has never imported, yet the plugin reloads and evaluates the unchanged server entry and its eager dependencies. For a large server bundle this repeats substantial work before development loader-data refresh can finish.

Requested: when Rsbuild can safely retain an evaluated server runtime across such a compilation, use that capability while still committing the new browser manifest and triggering Hot Data Revalidation (HDR), which refreshes route loader data after an edit.

Current behavior in the inspected versions: a new Node compilation normally calls loadBundle() again; Rsbuild creates a fresh runner. This is a feature request blocked on the core capability in Rsbuild #8496, not an implemented optimization or measured speedup.

Source path and versions

Inspected rsbuild-plugin-react-router 0.7.1 with Rsbuild 2.2.6, Rspack 2.2.4, and React Router 7.18.1. In finishAttempt, a changed Node compilation outside the CSS-only reuse exception calls evaluateServerBuilds. That calls server.environments.node.loadBundle(entryName) for every server entry, then the plugin rechecks whether the attempt is current and pairs the build with its accepted browser manifests.

Rsbuild 2.2.6 caches exports by compilation Stats and entry. It does not expose control over the runner's loaded outputs or later dynamic imports. Caching only the returned export object in the plugin would be unsafe when an already loaded chunk changes.

Evidence and reproduction limit

Rsbuild #8496 contains a complete public, executed core reproduction: editing an unused dynamic-import chunk leaves the entry output unchanged but recreates its eager object on the next loadBundle(). After the lazy chunk has been imported, keeping the old runner returns stale code on the next edit. That reproduction uses real Rsbuild compilations but no Router plugin or browser.

Application diagnostics motivate the integration: 15 server outputs totaling 52,173,231 bytes were reconstructed with identical contents across five cycles. A separate six-compilation capture found one changed deferred output among 3,156 executable outputs; it did not independently establish the later process's loaded-module set.

In a three-edit application run with a local Router coordination patch and a separate Rspack loader-dependency optimization, sampled main-isolate server compilation/evaluation averaged about 1.55 seconds per edit. This coarse category is not a removable-cost estimate. No runner-reuse implementation, public Router integration reproduction, or measured reuse saving is provided here.

Proposed integration boundary

Use the eventual core capability inside evaluateServerBuilds. Preserve the current compiler pairing, failure and supersession checks, and pinServerBuildsToManifests.

Even if the evaluated exports are reused, the committed Node compilation identity must advance to the newly accepted compilation. Otherwise the plugin can attach the wrong browser manifest or suppress the data refresh for an accepted edit. Commit all required server entries together, then acknowledge HDR for that accepted compilation.

Rsbuild should own loaded-output compatibility, freshness of future imports and runner lifetime. The plugin should own accepting matching browser/server results, manifest pairing and HDR. Until the core API guarantees compatibility, retain the existing fresh-load fallback.

Integration acceptance case

Build a development fixture whose server entry has an eager module with observable initialization and a lazy component that server rendering does not load. Load the component in the browser, then edit it:

  • If only an unused Node output changes, the server's eager module instance can be retained, while the browser manifest and HDR advance to the accepted compilation.
  • First server import of that lazy output returns the accepted code. Editing it after it has been loaded triggers a fresh runtime when required.
  • A loader body-only edit, with unchanged export names, produces fresh decoded loader data after HDR.
  • Removed/renamed chunks, manifest/CSS changes, overlapping compilations, evaluation errors and server shutdown preserve the last good build and do not publish stale results or leak runners.

These are proposed integration tests, not reported passing tests. Benchmark only after correctness is established, with diagnostic hashing disabled; the earlier all-output hashing capture itself took 1.76–2.04 seconds per compilation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedBlocked on an external dependency or upstream change

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions