rstackjs / rstackjs/rsbuild-plugin-react-router

[Perf]: integrate Rsbuild server-runtime reuse while preserving accepted generations and HDR

Open
#140 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

blocked
Dominant language
TypeScript
Stars
142
Forks
12
Avg merge
17h 2m
Merged PRs (30d)
27

Description

Request

Track the Router integration for a supported Rsbuild capability to retain an evaluated server runtime when every emitted output it has actually loaded remains unchanged. If any loaded output changes, use a fresh runtime. The associated core proposal is Rsbuild runner reuse.

This is a proposed capability, not an implemented optimization or measured speedup. It follows the Rsbuild maintainer's preference for avoiding unnecessary evaluation. The browser-settlement proposal discussed in #98 was not accepted by Rsbuild; its core prototype closed without merging.

Current plugin path

In 0.7.1, finishAttempt compares Node compilation identities. When Node changed and the CSS-only reuse exception does not apply, it calls evaluateServerBuilds. That calls server.environments.node.loadBundle(entryName) for every server entry. It then rechecks attempt supersession and commits builds pinned to the accepted browser manifests.

The audited Rsbuild 2.2.6 implementation caches bundle exports by Stats and entry; a new Stats produces a fresh runner with its own evaluated-module cache. The plugin receives exports, with no supported control over the runner's consumed-output set or future imports. Merely keeping an old export object in the plugin is not a safe implementation of runtime reuse.

Evidence motivating the core contract

In the recent matched run with the upstream loader-dependency optimization applied, sampled server-bundle construction/evaluation still accounted for about 1.55 s per edit. That coarse main-isolate category motivates this target; it is not a measured removable cost or a predicted runner-reuse speedup. The linked core proposal contains the matched timing table and profiling limits.

We observed a client component edit change only an unloaded async Node output while the already loaded server outputs stayed byte-identical. A separate constructor capture found 15 outputs totaling 52,173,231 bytes reconstructed in each of five generations, with identical bytes in all five. The largest entry was 46,348,289 bytes.

A later diagnostic compared all 3,156 executable outputs at six compiler completions. Exactly one async output changed; the remaining 3,155, entry mappings, and initial-file lists were unchanged. The earlier 15 consumed outputs matched their historical hashes. These are separate observations, not a claim that the second diagnostic measured the live consumed set. The all-output diagnostic added 1.76–2.04 s of hashing work per capture and is excluded from latency comparisons.

In a corresponding Vite 8.1.5 / React Router 7.18.1 run, a persistent SSR runner retained the transformed code and evaluated exports/promises of unaffected application modules while its virtual server entry acquired a new evaluation identity. The edited client component was never transformed/evaluated as SSR JavaScript; a stylesheet watch dependency still invalidated the root/entry path. Dynamic imports defer loading; they are not invalidation barriers. This is evidence for selective cache reuse, not semantic detection that “SSR is unnecessary.”

Why equality shortcuts are insufficient

  • Comparing every emitted file rejects every measured edit because the unloaded async output changed.
  • Comparing only the entry misses loaded chunks that changed.
  • Comparing a fixed historical list misses modules loaded later.
  • Keeping the old runner without a coherent output snapshot can mix generations when a later dynamic import reads changed files from the live filesystem.
  • Deferring evaluation to the next .data request moves the same work onto HDR; it does not establish reuse.

The minimum useful core capability therefore needs current consumed-output tracking, coherent resolution/bytes for later imports, and defined lifetime rules for in-flight requests. It may keep the entire runner when safe and replace the entire runner otherwise; per-source-module hot replacement is not required for this first step.

Proposed Router integration

Use the core capability at the existing evaluateServerBuilds boundary. The API name/shape below is deliberately left unspecified; the sequence is the requested behavior:

Keep all current attempt, compiler-cycle, error and one-sided-result checks.
For every required server entry:
  prepare a runtime candidate for the accepted Node compilation,
  optionally reusing a compatible previous runtime through the core API.
After awaiting candidates, recheck that this attempt is still current.
Validate route exports against this generation's browser manifests.
Pin manifest assets and atomically commit all entries as one generation.
Only after commit, acknowledge HDR intent for the accepted Node compilation.
Release superseded candidates according to the core ownership contract.

Preserve pinServerBuildsToManifests, last-good-build behavior, and supersession checks. A reused runtime's evaluated-artifact identity and the newly accepted compilation identity are different concepts: accepting a new compatible compilation must advance the committed Node identity/dependency snapshot, even when it reuses exports. Do not make it appear that only the previous Node compilation committed; that can suppress or misattribute HDR intent.

If the core capability is unavailable, retain the current loadBundle path. If it cannot guarantee compatibility, fall back to a fresh runtime. A callback that only says “outputs look equal” without binding to the accepted compilation is insufficient.

Reproduction/acceptance fixture

Create an SSR root that imports a server-safe application shell and declares a lazy component import that the initial SSR render does not invoke. Navigate to that component in the browser, then edit it while observing emitted outputs and runtime construction. With unchanged consumed server outputs, the accepted generation should reuse the runtime and still publish the matching browser manifest/HDR notification.

Also cover:

  1. A body-only loader change with unchanged export names: fresh decoded loader data after HDR.
  2. A changed previously loaded async output: fresh runtime before the new generation is used.
  3. A changed previously unloaded output: a later first import reads the correct accepted generation's bytes, followed by correct treatment once that module has been consumed.
  4. Removed/renamed chunks and entry/runtime mapping changes: safe fallback, no mixed generation.
  5. Overlapping compiles and requests: no superseded commit; old in-flight work retains a coherent runtime until release.
  6. Compilation/evaluation failure: last-good SSR remains usable and HDR intent survives recovery.
  7. Multiple server entries, browser manifest-only changes, server-only edits with unchanged web hash, and teardown/resource lifetime.

This plugin owns generation acceptance, manifest pinning, route/HDR policy, and the integration tests. Rsbuild owns reusable runner internals, module caches, and coherent future import behavior. PR #138 and PR #137 do not provide that development runtime capability. No runtime-reuse saving should be quoted until the new path passes these checks and a matched benchmark with diagnostics disabled.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at src/dev-generation.ts#finishAttempt and src/dev-runtime-artifacts.ts#evaluateServerBuilds, including pinServerBuildsToManifests, then review the linked Rsbuild runner-reuse proposal. The work is done when compatible consumed outputs reuse a coherent runtime, changed or uncertain outputs fall back safely, generation and HDR checks remain correct, and the listed overlapping, failure, manifest, and teardown cases pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
build-system, devtools, performance
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.