Add React Router plugin performance optimizations#39
Conversation
… perf/bundling-performance # Conflicts: # package.json # pnpm-lock.yaml # src/export-utils.ts # src/index.ts # src/plugin-utils.ts # src/route-chunks.ts # tests/export-utils.test.ts
commit: |
Document the performance optimization and benchmark tooling updates for release.
Co-authored-by: Matthew Davis <matthewdavis@openai.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f28c70be5b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR replaces the 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/plugin-utils.ts (1)
15-76:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle assignment patterns before allowing destructured exports through.
Line 603 uses this as the only guard for destructured exports, but
AssignmentPatternfalls through.export const { loader = fn } = objwould keep a server-only export in the client transform instead of failing closed.Proposed fix
export function validateDestructuredExports( id: AnyNode, exportsToRemove: readonly string[] ): void { + if (id.type === 'Identifier') { + if (exportsToRemove.includes(id.name)) { + throw invalidDestructureError(id.name); + } + return; + } + + if (id.type === 'RestElement') { + validateDestructuredExports(id.argument, exportsToRemove); + return; + } + + if (id.type === 'AssignmentPattern') { + validateDestructuredExports(id.left, exportsToRemove); + return; + } + if (id.type === 'ArrayPattern') { for (const element of id.elements ?? []) { if (!element) { continue; } - if ( - element.type === 'Identifier' && - exportsToRemove.includes(element.name) - ) { - throw invalidDestructureError(element.name); - } - - if ( - element.type === 'RestElement' && - element.argument.type === 'Identifier' && - exportsToRemove.includes(element.argument.name) - ) { - throw invalidDestructureError(element.argument.name); - } - - if (element.type === 'ArrayPattern' || element.type === 'ObjectPattern') { - validateDestructuredExports(element, exportsToRemove); - } + validateDestructuredExports(element, exportsToRemove); } } if (id.type === 'ObjectPattern') { @@ if (property.type === 'Property') { - if ( - property.value.type === 'Identifier' && - exportsToRemove.includes(property.value.name) - ) { - throw invalidDestructureError(property.value.name); - } - - if ( - property.value.type === 'ArrayPattern' || - property.value.type === 'ObjectPattern' - ) { - validateDestructuredExports(property.value, exportsToRemove); - } + validateDestructuredExports(property.value, exportsToRemove); } - if ( - property.type === 'RestElement' && - property.argument.type === 'Identifier' && - exportsToRemove.includes(property.argument.name) - ) { - throw invalidDestructureError(property.argument.name); + if (property.type === 'RestElement') { + validateDestructuredExports(property, exportsToRemove); } } } }Also applies to: 589-604
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugin-utils.ts` around lines 15 - 76, The validateDestructuredExports function does not handle AssignmentPattern nodes, which allows default values in destructured exports to bypass validation. When destructuring with default values like { loader = fn }, the AssignmentPattern case falls through without checking if the identifier should be removed. Add checks for AssignmentPattern in both the ArrayPattern and ObjectPattern iteration sections: for ArrayPattern elements that are AssignmentPattern, validate the argument property if it's an Identifier, and recursively validate if it's a nested pattern; similarly for ObjectPattern properties with AssignmentPattern values, validate the argument and recursively process nested patterns. This ensures server-only exports with default values are properly caught and fail closed.
🧹 Nitpick comments (3)
scripts/bench-builds.mjs (2)
300-327: ⚡ Quick winAdd legend clarifying metric semantics in Markdown output.
The plugin operations table (lines 300-323) displays
TotalandWallcolumns (line 304) but includes no legend explaining which metric accounts for concurrency. Perperformance-timing-semantics-analysis.md, readers need clear guidance on which metric to use for comparisons.Suggested addition before the table:
lines.push( '', `## Plugin Operations Breakdown`, '', '**Metric definitions:**', '- **Total (ms):** Cumulative wall time across all invocations (may overcount overlapping work)', '- **Wall (ms):** Wall-clock interval with overlap deduplication', '- **Max (ms):** Slowest single invocation (always accurate, no double-counting)', '', '| Environment | Operation | Count | Total | Wall | Max | Reports |', ... );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/bench-builds.mjs` around lines 300 - 327, The markdown table for plugin operations lacks a legend explaining the metrics, making it unclear which metric accounts for concurrency. Before pushing the table header with the markdown column separators in the lines array, add explanatory text that defines what each metric represents: Total should explain it is cumulative wall time across invocations, Wall should explain it deduplicates overlapping work, and Max should explain it is the slowest single invocation. Insert these legend lines into the lines array before the existing table header markdown lines to provide readers with clear guidance on metric semantics.
497-508: 💤 Low valueBenchmark failures are logged but allow process to continue.
Line 497-502 checks for any failed runs (non-zero status) and sets
process.exitCode = 1, but the script continues afterwriteOutputs()at line 492. This means:
- Results file is written even if builds failed
- Process exits with code 1 only after all benchmarks complete
- If
--fail-fastis NOT enabled, a single build failure doesn't stop the loopThis is generally correct (allow all benchmarks to run, report overall status), but may be confusing if a partial failure occurs — the output file will contain failed runs mixed with passed runs without a clear signal in the file itself.
Consider adding a
failedflag to the result JSON so downstream comparison tools can detect and warn on partial failures.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/bench-builds.mjs` around lines 497 - 508, The benchmark results are written to output regardless of whether failures occurred, making it difficult for downstream tools to detect partial failures. Add a `failed` boolean flag to the benchmark results object (the structure being passed to `writeOutputs()`) that gets set to true when the condition at line 497 detects any runs with non-zero status. This flag should be determined using the same logic that checks if benchmarks.some(benchmark => benchmark.runs.some(run => run.status !== 0)), allowing the output JSON file to include a clear indicator of whether the benchmark run had any failures.src/manifest.ts (1)
223-225: ⚡ Quick winConsider failing the build on route analysis errors in production builds.
When
getRouteModuleAnalysisthrows, the error is logged but the build continues with empty exports. This produces an incorrect manifest wherehasAction,hasLoader, etc., will all befalseregardless of what the route actually exports. In dev mode this may be acceptable for hot-reload resilience, but in build mode (isBuild: true) this could cause silent runtime failures.Consider re-throwing in build mode:
Suggested approach
} catch (error) { console.error(`Failed to analyze route file ${routeFilePath}:`, error); + if (isBuild) { + throw error; + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/manifest.ts` around lines 223 - 225, The catch block for the getRouteModuleAnalysis error logs the error but allows the build to continue, resulting in an incorrect manifest with all route properties set to false. In the catch block where the error is logged, add a check for isBuild mode and re-throw the error if isBuild is true, ensuring the build fails on route analysis errors in production builds while remaining resilient in dev mode. Keep the current logging behavior but add the conditional re-throw after the console.error call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@benchmarks/chunk-precompute-methodology.md`:
- Around line 69-71: The fenced code blocks at lines 69 and 341 in the markdown
file lack language tags, triggering the MD040 markdownlint rule. Add the
language tag `text` to both unlabeled code blocks by changing the opening triple
backticks from ``` to ```text for the array example and the metrics table
example to satisfy linting requirements.
- Around line 121-123: The documentation contains a hardcoded machine-specific
absolute path `/home/zack/projects/rsbuild-plugin-react-router` on line 122 that
will not be valid on other developers' machines or environments. Replace this
specific absolute path with a relative reference or generic placeholder that
conveys the same meaning (such as simply referring to "the repo root" or using a
relative path notation like `.`) to make the instructions portable across
different machines and development environments.
In `@package.json`:
- Line 85: Remove the unused rspack-plugin-virtual-module dependency from
package.json by deleting the line "rspack-plugin-virtual-module": "^1.0.1" since
virtual module handling has been migrated to
rspack.experiments.VirtualModulesPlugin. After removing this line from
package.json, also update the lockfile to remove any references to this package
by running the appropriate package manager command (npm install or yarn install
depending on which lockfile is being used).
In `@performance-timing-semantics-analysis.md`:
- Around line 118-122: Add a documentation comment or docstring to
`src/performance.ts` that explains the timing semantics of the performance
profiler fields. The documentation should clarify that totalMs is measured using
wall-clock deltas captured at record() calls and that due to concurrent async
transforms interleaving on the Node.js event loop, totalMs double-counts
overlapping wait time and can exceed the actual serial cost or even
compilerLifecycleMs. Include guidance that totalMs should be treated as an upper
bound, not precise attribution; explain which fields remain accurate under
concurrency (count, maxMs, slowest[], and compilerLifecycleMs); and advise users
to use the interval-union wallMs aggregate instead of totalMs for
concurrency-safe cost attribution across operations. You can paste the
ready-to-paste paragraph provided in the analysis document or adapt it to fit
your code style.
In `@route-analysis-duplication-audit.md`:
- Around line 1-170: The audit document contains stale line number references
that no longer match the current codebase—for example, cache declarations in
export-utils.ts are documented at lines 24–29 but actually exist at lines 49–55,
and getReactRouterManifestForDev is documented at line 110 but is actually at
line 147. Update all line number references throughout the document by
cross-referencing the specified files (export-utils.ts, manifest.ts, index.ts,
modify-browser-manifest.ts, build-manifest.ts) and the named functions and
caches (transformCache, exportNamesCache, routeModuleAnalysisCache,
getReactRouterManifestForDev, getRouteModuleAnalysis,
detectRouteChunksIfEnabled, etc.) to match their actual current locations.
Additionally, add a header note specifying the current commit hash or date when
the line numbers were verified to prevent future confusion.
In `@src/export-utils.ts`:
- Around line 183-197: The collectExportNames function is missing support for
TypeScript enum declarations, which are runtime exports that should be tracked.
Add a new else if condition after the FunctionDeclaration and ClassDeclaration
check to handle TSEnumDeclaration types. Follow the same pattern as
FunctionDeclaration and ClassDeclaration by checking if the declaration type is
'TSEnumDeclaration' and if declaration.id?.name exists, then add the name to the
exportNames set. This ensures exported enums are properly recorded alongside
other exported declarations.
In `@src/index.ts`:
- Around line 1365-1374: In the bundleId assignment on the line with the replace
method call, the regex pattern is incorrectly escaping the dot character. Change
the regex pattern from /\\.js$/ to /\.js$/ so that it correctly matches and
removes the actual `.js` suffix from the captured bundle ID string. This ensures
that when the module loader appends `.js` to virtual module requests, the
bundleId value is properly stripped (e.g., from `admin.js` to `admin`) so that
subsequent lookups in latestServerManifestsByBundleId work correctly.
In `@src/plugin-utils.ts`:
- Around line 784-829: The component wrapping logic in this export processing
block has gaps that skip certain valid component exports. First, remove the
condition checking expr.type !== 'ClassDeclaration' in the
ExportDefaultDeclaration branch (around line 791) so that default class exports
are also wrapped with the withComponentProps HOC. Second, add handling for
ExportNamedDeclaration statements that contain ExportSpecifier elements (such as
export { ErrorBoundary } or export { ErrorBoundary as Component }) to wrap those
component exports with the appropriate HOC. Third, extend the
FunctionDeclaration handling in the ExportNamedDeclaration branch to also handle
ClassDeclaration exports in the same way, ensuring that named class exports are
wrapped with the with*Props HOC just like function exports.
- Around line 550-566: The export removal logic in the loop that processes
program.body statements only handles ExportNamedDeclaration but ignores
ExportAllDeclaration (export * from '...'). This allows export * statements to
remain in the bundle even when they should be removed. Add a check for
statement.type === 'ExportAllDeclaration' alongside the existing
ExportNamedDeclaration check, and remove the entire ExportAllDeclaration
statement when its source module contains exports that are marked for removal in
the exportsToRemove set. This ensures export * statements don't leak server-only
exports into the web bundle.
---
Outside diff comments:
In `@src/plugin-utils.ts`:
- Around line 15-76: The validateDestructuredExports function does not handle
AssignmentPattern nodes, which allows default values in destructured exports to
bypass validation. When destructuring with default values like { loader = fn },
the AssignmentPattern case falls through without checking if the identifier
should be removed. Add checks for AssignmentPattern in both the ArrayPattern and
ObjectPattern iteration sections: for ArrayPattern elements that are
AssignmentPattern, validate the argument property if it's an Identifier, and
recursively validate if it's a nested pattern; similarly for ObjectPattern
properties with AssignmentPattern values, validate the argument and recursively
process nested patterns. This ensures server-only exports with default values
are properly caught and fail closed.
---
Nitpick comments:
In `@scripts/bench-builds.mjs`:
- Around line 300-327: The markdown table for plugin operations lacks a legend
explaining the metrics, making it unclear which metric accounts for concurrency.
Before pushing the table header with the markdown column separators in the lines
array, add explanatory text that defines what each metric represents: Total
should explain it is cumulative wall time across invocations, Wall should
explain it deduplicates overlapping work, and Max should explain it is the
slowest single invocation. Insert these legend lines into the lines array before
the existing table header markdown lines to provide readers with clear guidance
on metric semantics.
- Around line 497-508: The benchmark results are written to output regardless of
whether failures occurred, making it difficult for downstream tools to detect
partial failures. Add a `failed` boolean flag to the benchmark results object
(the structure being passed to `writeOutputs()`) that gets set to true when the
condition at line 497 detects any runs with non-zero status. This flag should be
determined using the same logic that checks if benchmarks.some(benchmark =>
benchmark.runs.some(run => run.status !== 0)), allowing the output JSON file to
include a clear indicator of whether the benchmark run had any failures.
In `@src/manifest.ts`:
- Around line 223-225: The catch block for the getRouteModuleAnalysis error logs
the error but allows the build to continue, resulting in an incorrect manifest
with all route properties set to false. In the catch block where the error is
logged, add a check for isBuild mode and re-throw the error if isBuild is true,
ensuring the build fails on route analysis errors in production builds while
remaining resilient in dev mode. Keep the current logging behavior but add the
conditional re-throw after the console.error call.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d22b41ff-52e3-4adc-ab3f-0b8c033cc04d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (46)
.changeset/bright-routes-run.md.changeset/fast-routes-dance.md.gitignorebenchmarks/README.mdbenchmarks/chunk-precompute-methodology.mdbenchmarks/manifest-performance-methodology.mdconfig/rslib.config.tspackage.jsonperformance-timing-semantics-analysis.mdroute-analysis-duplication-audit.mdroute-chunk-parse-traverse-analysis.mdscripts/bench-builds.mjsscripts/bench-client-entry-analysis.mjsscripts/benchmark-yuku.mjsscripts/benchmark/fixture.mjsscripts/compare-benchmarks.mjsscripts/compare-client-entry-analysis.mjssrc/babel.tssrc/constants.tssrc/export-utils.tssrc/index.tssrc/manifest.tssrc/modify-browser-manifest.tssrc/performance.tssrc/plugin-utils.tssrc/route-artifacts.tssrc/route-chunks.tssrc/templates/entry.server.tsxsrc/types.tssrc/virtual-modules.tstask/lexer-route-export-triage.mdtask/route-chunk-correctness-test-spec.mdtask/route-chunk-precompute-plan.mdtask/unified-route-module-analysis-cache-triage.mdtests/benchmark-fixture.test.tstests/export-utils.test.tstests/features.test.tstests/index.test.tstests/manifest-split-route-modules.test.tstests/manifest.test.tstests/performance.test.tstests/remove-exports.test.tstests/route-artifacts.test.tstests/route-chunks-cache.test.tstests/route-chunks.test.tstests/setup.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/index.test.ts (1)
35-65: 💤 Low valueConsider adding test for explicit
lazyCompilation: true.The test suite covers the default behavior (implicit
true), explicit object configuration, and explicitfalse, but not an explicitlazyCompilation: true. While the default test already validatestruebehavior, an explicit test would distinguish between "default true" and "user-provided true" for clarity.🧪 Optional test case
it('should respect explicit lazy compilation true', async () => { const rsbuild = await createStubRsbuild({ rsbuildConfig: {}, }); rsbuild.addPlugins([pluginReactRouter({ lazyCompilation: true })]); const config = await rsbuild.unwrapConfig(); expect(config.dev.lazyCompilation).toBe(true); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/index.test.ts` around lines 35 - 65, Add a new test case to the test suite that validates the behavior when lazyCompilation is explicitly set to the boolean value true. Create a test following the same pattern as the existing tests in tests/index.test.ts (using createStubRsbuild, addPlugins with pluginReactRouter, and unwrapConfig), but pass lazyCompilation: true as a boolean to distinguish between the default implicit true behavior and an explicit user-provided true value. Assert that config.dev.lazyCompilation equals true in this case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/index.test.ts`:
- Around line 35-65: Add a new test case to the test suite that validates the
behavior when lazyCompilation is explicitly set to the boolean value true.
Create a test following the same pattern as the existing tests in
tests/index.test.ts (using createStubRsbuild, addPlugins with pluginReactRouter,
and unwrapConfig), but pass lazyCompilation: true as a boolean to distinguish
between the default implicit true behavior and an explicit user-provided true
value. Assert that config.dev.lazyCompilation equals true in this case.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 56e43e66-4109-4ba7-a17e-6ec8d1c71821
📒 Files selected for processing (4)
scripts/benchmark/fixture.mjssrc/index.tssrc/types.tstests/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/types.ts
- scripts/benchmark/fixture.mjs
- src/index.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
examples/default-template/tests/e2e/dev-route-watch.test.ts (1)
124-131: ⚡ Quick winFail fast if the route insertion anchor is missing.
Line 127 silently leaves
routes.tsunchanged when the anchor comment is absent, then the test fails later via timeout. Add an explicit anchor check so this fails immediately with a clear error.Suggested hardening
const routesConfig = readFileSync(routesConfigPath, 'utf8'); + const anchor = ' // Docs section with nested routes'; + if (!routesConfig.includes(anchor)) { + throw new Error(`Missing expected insertion anchor in ${routesConfigPath}`); + } writeFileSync( routesConfigPath, routesConfig.replace( - ' // Docs section with nested routes', - `${addedRouteConfigEntry}\n\n // Docs section with nested routes` + anchor, + `${addedRouteConfigEntry}\n\n${anchor}` ) );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/default-template/tests/e2e/dev-route-watch.test.ts` around lines 124 - 131, The code silently continues if the anchor comment is missing from the routes configuration, causing test failures later with confusing timeout errors. Before the writeFileSync call that uses routesConfig.replace(), add an explicit check to verify that the anchor comment string ' // Docs section with nested routes' exists in the routesConfig. If the anchor is not found using an includes() or indexOf() check, throw an error immediately with a clear message indicating the anchor comment is missing from the routes file, so failures are caught and diagnosed right away instead of during test execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/route-watch.ts`:
- Around line 216-231: The runRescan function checks if closed is true at the
start, but if close() executes while awaiting readRouteDirectoryState (line
221), the function continues and can still call syncDirectoryWatchers and
touchRestartMarker, creating FSWatchers after shutdown. Add a check for the
closed flag immediately after the await completes and before calling
syncDirectoryWatchers to prevent post-close side effects and watcher leaks. This
same pattern should be applied to the other location mentioned at lines 254-263.
---
Nitpick comments:
In `@examples/default-template/tests/e2e/dev-route-watch.test.ts`:
- Around line 124-131: The code silently continues if the anchor comment is
missing from the routes configuration, causing test failures later with
confusing timeout errors. Before the writeFileSync call that uses
routesConfig.replace(), add an explicit check to verify that the anchor comment
string ' // Docs section with nested routes' exists in the routesConfig. If the
anchor is not found using an includes() or indexOf() check, throw an error
immediately with a clear message indicating the anchor comment is missing from
the routes file, so failures are caught and diagnosed right away instead of
during test execution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bb9ee7a1-447c-449f-8ea4-b512716636b8
📒 Files selected for processing (7)
examples/default-template/playwright.config.tsexamples/default-template/tests/e2e/dev-route-watch.test.tssrc/index.tssrc/route-watch.tstests/index.test.tstests/route-watch.test.tstests/setup.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/index.test.ts
- src/index.ts
|
I've been working on the precedence-aware printer in yuku, and it's been released in the latest yuku So I just had to mention it here when I saw aa33f69. Now |
Summary
lazyCompilationplugin option that forwards to Rsbuild's dev-onlydev.lazyCompilationconfig when explicitly set.Performance
Latest local benchmark highlights:
Yuku microbench after consolidation:
Lazy compilation note:
lazyCompilationis dev-only in Rsbuild.Test plan
pnpm test:core tests/index.test.ts tests/remove-exports.test.tspnpm buildCI=true pnpm --filter './examples/client-only' test:e2epnpm testpnpm test tests/index.test.ts tests/benchmark-fixture.test.tspnpm bench --profile default --filter synthetic-256-spa --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/spa-abort-request-finalREACT_ROUTER_BENCHMARK_LAZY_COMPILATION=1 pnpm bench --profile default --filter synthetic-256-ssr-esm --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/lazy-on-ssr-esm