Skip to content

fix(node-renderer): expose performance in VM context when supportModules#3158

Merged
justin808 merged 5 commits intomainfrom
jg/3154-vm-performance
Apr 19, 2026
Merged

fix(node-renderer): expose performance in VM context when supportModules#3158
justin808 merged 5 commits intomainfrom
jg/3154-vm-performance

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Apr 18, 2026

Summary

  • Adds performance to the default globals injected into the node renderer's VM context when supportModules: true.
  • React 19's development build of React.lazy calls performance.now() inside lazyInitializer, which threw ReferenceError: performance is not defined without this — making React.lazy unusable in dev unless users manually passed performance via additionalContext.
  • Updates the Config JSDoc, the js-configuration.md docs, and the CHANGELOG, and adds a regression test.

Fixes #3154.

Notes

The reporter also raised a broader question about the recommended pattern for React.lazy with streaming SSR and LimitChunkCountPlugin({ maxChunks: 1 }) (intermittent Suspense resolution causing hydration mismatches / React error #419). That's a separate, larger discussion and is not addressed here — this PR only fixes the concrete performance VM-context bug the reporter identified and worked around.

Test plan

  • pnpm --filter react-on-rails-pro-node-renderer exec jest vm.test — all 27 tests pass, including the new assertions that performance and performance.now are defined in the VM when supportModules is enabled.
  • pnpm exec prettier --check on the modified files — passes.
  • pnpm -w run eslint on the modified source files — passes.

Generated with Claude Code (https://claude.com/claude-code)


Note

Low Risk
Low risk: adds one additional global (performance) to the VM context behind the existing supportModules flag, plus doc/test updates; main behavioral change is potential nondeterministic SSR output if app code embeds performance.now() values.

Overview
Fixes React 19 dev SSR crashes by adding the host performance object to the node renderer VM globals when supportModules: true, alongside existing injected Node globals.

Updates docs and JSDoc to reflect the new default global set and to clarify that stubTimers does not stub performance (with guidance for overriding via additionalContext), and extends VM tests to assert performance/performance.now presence when enabled (and absence when disabled).

Reviewed by Cursor Bugbot for commit 54b4aaf. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes

    • Restored React 19 dev compatibility by ensuring the renderer VM includes the performance global when module support is enabled, preventing ReferenceError in lazy-loaded code.
  • Documentation

    • Clarified that performance is included in the VM globals when module support is enabled and noted behavior relative to timer stubbing and SSR determinism.
  • Tests

    • Updated tests to assert performance and performance.now availability in the VM sandbox.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Node’s performance global to the renderer VM sandbox when supportModules: true; updates docs and CHANGELOG and tightens VM tests to assert performance presence and that performance.now is a function to avoid ReferenceError in React 19 dev builds.

Changes

Cohort / File(s) Summary
Changelog & Docs
CHANGELOG.md, docs/oss/building-features/node-renderer/js-configuration.md
Changelog entry and docs updated to state that Node’s real performance is included in the default VM globals when supportModules: true; note that performance is unaffected by stubTimers and can be overridden via additionalContext.
Config comment
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts
Comment/docs on the exported Config (supportModules) updated to list performance among the documented VM globals; no API/signature changes.
VM implementation & tests
packages/react-on-rails-pro-node-renderer/src/worker/vm.ts, packages/react-on-rails-pro-node-renderer/tests/vm.test.ts
buildVM() now injects Node performance into the VM sandbox when supportModules is enabled. Tests changed to assert exact typeof results and to verify performance availability and that performance.now is a function when enabled (and undefined when disabled).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I slipped a clock into the sandbox light,
so lazy bits can tick and stream just right.
A carrot for timing, a hop and a grin,
now Suspense can measure where it begins. 🎋

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: exposing the performance object in the VM context when supportModules is enabled, directly addressing the fix implemented across all modified files.
Linked Issues check ✅ Passed The PR fully addresses the concrete coding requirement from #3154: adding the performance global to the default VM context when supportModules: true to fix React 19's ReferenceError in React.lazy development code, along with comprehensive test coverage and documentation updates.
Out of Scope Changes check ✅ Passed All changes are directly scoped to exposing performance in the VM context: CHANGELOG entry, configuration documentation, JSDoc updates, VM implementation, and regression tests—with no unrelated modifications.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/3154-vm-performance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 18, 2026

Code Review

Overview

This is a clean, well-scoped fix. React 19's dev build calls performance.now() inside lazyInitializer, and the VM sandbox didn't include it — causing a ReferenceError for any project using React.lazy with supportModules: true. Adding performance to the default context object (alongside the already-exposed process, Buffer, etc.) is the right fix.

What's Good

  • Minimal diff — one line in the right place, plus consistent doc/JSDoc/changelog updates.
  • Followed the existing convention — the comment at vm.ts:217 explicitly lists what to update when adding globals, and both referenced files were updated.
  • Regression test added — the new assertions verify both typeof performance !== "undefined" and typeof performance.now === "function", which covers the exact React 19 call path.

Issues

Minor: Missing negative assertion in the supportModules: false test

The 'not available if supportModules disabled' test (vm.test.ts:30–42) checks that Buffer and process are absent when supportModules is false, but it doesn't add a matching assertion for performance. Without it, a future regression that accidentally injects performance outside of the supportModules branch wouldn't be caught by the existing test suite.

Suggested addition (see inline comment below).

Design note: stubTimers does not stub performance

When stubTimers: true, the VM stubs out all timer functions (setTimeout, setInterval, etc.) with no-ops, making SSR deterministic. performance.now() is also timing-related — a component that stores performance.now() in rendered output (e.g., for waterfall tracing) would produce non-deterministic HTML across renders, potentially causing hydration mismatches.

This is a pre-existing design question (not introduced here), but it's worth noting in a follow-up or in the docs. Acceptable to defer.

Informational: performance is shared with the host process

Like process, the injected performance is the real host object — VM code can call performance.mark() / performance.measure() and those entries will land in the host's performance timeline. This is consistent with how process works today and is a known trade-off of this VM design. No action needed, just awareness.

Summary

The fix is correct and production-ready. The only actionable item before merge is adding the complementary negative assertion in the disabled test.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

Adds performance to the default globals injected into the node renderer's VM context when supportModules: true, fixing a ReferenceError: performance is not defined thrown by React 19's dev build of React.lazy (which calls performance.now() in lazyInitializer). The change is a one-liner in vm.ts, accompanied by updated JSDoc, docs, changelog, and tightened regression tests.

Confidence Score: 5/5

Safe to merge — minimal, well-tested one-liner fix with no regressions possible for users who don't set supportModules: true.

All changes are P2 or lower. The core fix is a single-line addition of the Node.js global performance to an existing opt-in code path. Tests are thorough and the assertion tightening (toBeTruthy → toBe('true')) is a genuine correctness improvement. Docs, JSDoc, and changelog are all updated consistently.

No files require special attention.

Important Files Changed

Filename Overview
packages/react-on-rails-pro-node-renderer/src/worker/vm.ts Adds performance to the extendContext object inside the supportModules branch — correct placement, matching the existing pattern for all other globals.
packages/react-on-rails-pro-node-renderer/tests/vm.test.ts Renames describe block, adds assertions for performance and performance.now availability, and tightens existing assertions from toBeTruthy() to toBe('true') — a real correctness improvement since runInVM returns strings.
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts JSDoc comment updated to include performance in the listed globals — accurate and consistent with the implementation change.
docs/oss/building-features/node-renderer/js-configuration.md Docs updated with performance in the globals list and a helpful note explaining the React 19 motivation — clear and accurate.
CHANGELOG.md Changelog entry added under Fixed with correct issue/PR links and author attribution — follows project conventions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildVM called] --> B{supportModules enabled?}
    B -- No --> C[Minimal context\nno Buffer, process,\nperformance, etc.]
    B -- Yes --> D[extendContext with defaults:\nBuffer, TextDecoder, TextEncoder,\nURLSearchParams, ReadableStream,\nprocess, performance NEW,\nsetTimeout, setInterval, setImmediate,\nclearTimeout, clearInterval,\nclearImmediate, queueMicrotask]
    D --> E{additionalContext set?}
    C --> E
    E -- Yes --> F[extendContext with\nadditionalContext overrides]
    E -- No --> G[vm.createContext]
    F --> G
    G --> H[VM ready for runInVM]
    H --> I[React 19 React.lazy calls\nperformance.now works]
Loading

Reviews (2): Last reviewed commit: "test: address review nits on performance..." | Re-trigger Greptile

Comment thread packages/react-on-rails-pro-node-renderer/tests/vm.test.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 18, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.76 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.76 KB (0%)
react-on-rails/client bundled (brotli) 53.86 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.89 KB (+0.06% 🔺)
react-on-rails-pro/client bundled (gzip) 63.71 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 63.71 KB (0%)
react-on-rails-pro/client bundled (brotli) 54.63 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.63 KB (0%)
registerServerComponent/client bundled (gzip) 127.53 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.53 KB (0%)
registerServerComponent/client bundled (brotli) 61.7 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.7 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.73 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.73 KB (0%)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 29: Update the CHANGELOG.md entry that begins "**[Pro]** **Node renderer
now exposes `performance` when `supportModules: true`**" to include the PR
attribution suffix in the required format; append " [PR
3158](https://github.com/shakacode/react_on_rails/pull/3158) by
[username](https://github.com/username)" (replacing <username> with the actual
PR author) so the line matches the project's changelog format.

In `@packages/react-on-rails-pro-node-renderer/tests/vm.test.ts`:
- Around line 59-63: The tests call runInVM which serializes boolean results to
strings, so using toBeTruthy() can mask failures; change the two assertions that
check performance existence to assert the exact serialized value (e.g.
expect(result).toBe("true")) for both invocations that use runInVM('typeof
performance !== "undefined"', uploadedBundlePathForTest()) and runInVM('typeof
performance.now === "function"', uploadedBundlePathForTest()) so the regression
fails when the VM returns "false".
🪄 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

Run ID: e2415cca-2608-4a66-896c-8279d9345410

📥 Commits

Reviewing files that changed from the base of the PR and between e9e27a2 and 354964c.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • docs/oss/building-features/node-renderer/js-configuration.md
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts
  • packages/react-on-rails-pro-node-renderer/src/worker/vm.ts
  • packages/react-on-rails-pro-node-renderer/tests/vm.test.ts

Comment thread CHANGELOG.md Outdated
Comment thread packages/react-on-rails-pro-node-renderer/tests/vm.test.ts Outdated
…dules

React 19's development build of `React.lazy` calls `performance.now()`
inside `lazyInitializer` for I/O profiling. This threw
`ReferenceError: performance is not defined` in the node renderer's VM
context and made `React.lazy` unusable in development unless users
manually added `performance` via `additionalContext`.

Adds `performance` to the default globals injected when
`supportModules: true`, alongside the other Node.js standard globals
(`Buffer`, `process`, timers, etc.). No user config change required.

Fixes #3154.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justin808 justin808 force-pushed the jg/3154-vm-performance branch from 354964c to 37ffb63 Compare April 18, 2026 07:21
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37ffb63c15

ℹ️ 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".

Comment thread packages/react-on-rails-pro-node-renderer/tests/vm.test.ts Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 18, 2026

Code Review

Overall: Approve with minor suggestions

Clean, well-scoped fix. Node.js VM contexts do not inherit host globals, so React 19 performance.now() inside lazyInitializer rightfully throws unless performance is explicitly injected. The one-line addition to vm.ts is the correct fix.

Strengths

  • Minimal diff: one runtime line, with docs/JSDoc/changelog all updated in sync
  • The IMPORTANT comment in vm.ts (line 217) prompted updating both docs and configBuilder, and both were updated
  • Regression test is properly scoped under supportModules: true

Issues

Missing negative-case assertion (minor)
The test block "not available if supportModules disabled" checks Buffer and process are absent, but does not assert performance is absent too. A future change could accidentally leak performance into the unsandboxed path without the test suite catching it.

Test describe block name is stale (nit)
The describe block is called Buffer and process in context but now covers performance too. Worth renaming to avoid confusion.

Documentation ordering (nit)
In js-configuration.md, the new React 19 note appears after the Setting this value to false... sentence, which breaks the logical flow. Moving it directly after the globals list reads more naturally.

No concerns on

  • Security: performance exposes timing data but this runs in a trusted SSR worker, not a meaningful attack surface
  • Node.js compatibility: performance is a stable global since Node 16; React 19 requires Node 18+, so there is no compatibility regression
  • Correctness: matches how process, Buffer, and other globals are already exposed

Comment thread docs/oss/building-features/node-renderer/js-configuration.md
- CHANGELOG: add PR attribution suffix to match repository convention
- vm.test.ts: rename describe block to include `performance`
- vm.test.ts: add symmetric `performance === "undefined"` assertion
  to the `supportModules disabled` test so a future regression leaking
  `performance` outside the guarded branch is caught
- vm.test.ts: tighten assertions from `toBeTruthy()` to `toBe('true')`
  since `runInVM` serializes booleans to strings (`"false"` would
  otherwise still pass `toBeTruthy()`)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justin808
Copy link
Copy Markdown
Member Author

Review feedback addressed

Addressed all actionable review comments in 8c630ea. Summary:

Addressed in code:

  • CHANGELOG attribution (@coderabbitai[bot]) — appended [PR 3158](https://github.com/shakacode/react_on_rails/pull/3158) by [justin808](https://github.com/justin808). so the entry matches repository conventions.
  • Symmetric performance === "undefined" assertion (@claude[bot], @greptile-apps[bot]) — added to the supportModules disabled test so a future regression that leaks performance outside the guarded branch gets caught.
  • Tighter toBe('true') assertions (@coderabbitai[bot]) — runInVM serializes booleans to strings, so toBeTruthy() would accept a string "false". Swapped to toBe('true') for both positive and negative assertions across the describe block.
  • describe block renamed (@greptile-apps[bot]) — 'Buffer and process in context''default VM globals (Buffer, process, performance)' to reflect all tested globals.

Verified tests pass: all 27 tests in packages/react-on-rails-pro-node-renderer/tests/vm.test.ts pass locally.

Acknowledged, not acted on:

  • stubTimers doesn't stub performance (@claude[bot]) — agreed this is a pre-existing design question worth thinking about, but as the reviewer noted, it's "acceptable to defer" and out of scope for this targeted fix. Can be revisited in a follow-up if hydration mismatches from performance.now()-in-render surface in practice.
  • performance is shared with the host process (@claude[bot]) — confirmed informational; this mirrors how process and Buffer already behave in the default context, so the trade-off is consistent with existing design.

Not actionable: CodeRabbit walkthrough summary, Greptile summary, and the size-limit bot report are auto-generated status posts.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Tip

For best results, initiate chat on the files or code changes.

@justin808 — thank you for the thorough write-up. Every actionable item from my review has been handled cleanly:

  • The toBe('true') tightening is exactly right — toBeTruthy() would pass on the string "false", so the stricter assertion is meaningfully better.
  • The symmetric performance === "undefined" guard in the disabled-branch test is a solid regression net.
  • The CHANGELOG entry now matches the repo convention (confirmed via the learning stored from a prior PR).
  • The deferred items (stubTimers/performance isolation, shared-host performance) are pre-existing design questions consistent with how process and Buffer already behave — deferring is the right call here.

This looks good to merge. 🚀

[approve]


🧠 Learnings used
Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/packages/react-on-rails-pro-node-renderer/**/*.test.{ts,tsx,js,jsx} : Node renderer tests should be run with: `pnpm --filter react-on-rails-pro-node-renderer run test`

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 2850
File: docs/oss/api-reference/rails-view-rendering-from-inline-javascript.md:0-0
Timestamp: 2026-03-26T07:01:43.653Z
Learning: In `packages/react-on-rails/src/ClientRenderer.ts`, the duplicate-render guard (which skips rendering if the DOM node is already connected/mounted) applies only to the automatic page-load rendering path. The public `ReactOnRails.render(...)` API routes through `packages/react-on-rails/src/base/client.ts` and calls `reactHydrateOrRender(...)` directly, without any deduplication guard. Therefore, repeated calls to the public `ReactOnRails.render(...)` API on the same DOM node (e.g., `#root`) WILL invoke React again — they are NOT deduplicated. Documentation stating this behavior is correct and should not be flagged.

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 2651
File: docs/oss/introduction.md:47-47
Timestamp: 2026-03-26T02:20:12.940Z
Learning: In `docs/oss/introduction.md` (and related docs in the `shakacode/react_on_rails` repo), the link to `docs/oss/getting-started/comparison-with-alternatives.md` described as "a side-by-side matrix with benchmarks" is intentionally and accurately worded. The page includes actual benchmark sections (e.g., Vite vs Rspack benchmark tables), so the phrase "with benchmarks" should NOT be flagged as misleading in future reviews.

Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 2464
File: packages/react-on-rails-pro-node-renderer/src/worker.ts:343-393
Timestamp: 2026-02-21T20:46:38.404Z
Learning: In packages/react-on-rails-pro-node-renderer, the `Promise.all` pattern for per-bundle operations (e.g., in `/upload-assets` endpoint and `handleNewBundlesProvided` in handleRenderRequest.ts) is intentional. Copy operations like `copyUploadedAssets` are idempotent and retries are safe, so fast-fail behavior on lock acquisition or copy errors is acceptable. Switching to `Promise.allSettled` to surface partial success would require Ruby-side protocol changes.

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 3094
File: react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb:40-64
Timestamp: 2026-04-13T02:28:35.099Z
Learning: In `react_on_rails_pro/lib/react_on_rails_pro/js_code_builder.rb`, the `typeof generateRSCPayload !== 'function'` guards in `rsc_payload_function_section` are an intentional faithful port of the existing pattern in `server_rendering_js_code.rb` (lines 21 and 39). Do not flag this guard as a bug in this file; any fix should be tracked as a separate issue covering both files.

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T23:15:11.806Z
Learning: Applies to CHANGELOG.md : Changelog format: `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)` (no hash before PR number)

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/dummy/commonWebpackConfig.js : Changes to `commonWebpackConfig.js` affect all three webpack bundles (client, server, and RSC) in the Pro dummy app

Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 2456
File: packages/react-on-rails-pro-node-renderer/tests/uploadRaceCondition.test.ts:43-61
Timestamp: 2026-02-21T09:29:56.939Z
Learning: The react-on-rails-pro-node-renderer test suite uses a custom Jest timeout of 30 seconds (configured in Jest config), not Jest's default 5-second timeout.

Learnt from: justin808
Repo: shakacode/react_on_rails PR: 0
File: :0-0
Timestamp: 2026-04-13T02:29:20.920Z
Learning: In `react_on_rails_pro/lib/react_on_rails_pro/engine.rb` and `react_on_rails/lib/react_on_rails/configuration.rb` (PR `#3094`): Do NOT reset `rendering_strategy` or `js_code_builder` inside `ReactOnRails.configure`. The Pro engine sets these exactly once during `after_initialize`; resetting them on reconfigure would silently drop Pro's NodeStrategy/ProJsCodeBuilder assignments. A proper reconfigure mechanism for these accessors is deferred to Step 3 of the strategy-pattern refactor (`#2905`).

Move the React 19 `performance.now()` explanatory note to sit
immediately after the globals list so it doesn't interrupt the
option's main description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justin808
Copy link
Copy Markdown
Member Author

Review round summary (I, Claude Code)

Addressed the remaining review feedback on top of commit 8c630ea:

Commit d411f2adocs: reorder performance note in supportModules section

  • Moved the React 19 performance.now() explanatory note in docs/oss/building-features/node-renderer/js-configuration.md to sit immediately after the globals list, so the main option description ("required to equal true ... loadable components" → "Setting this value to false...") is no longer interrupted (per @claude[bot]'s flow suggestion).

Not a code change@chatgpt-codex-connector[bot]'s toBeTruthy()toBe('true') suggestion was a duplicate of CodeRabbit's parallel thread; both were already resolved in commit 8c630ea where all assertions in the default VM globals describe block were strengthened. Replied inline and resolved the thread.

All review threads are now resolved.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 18, 2026

Code Review

Overview

Minimal, well-scoped fix: adds performance to the Node.js VM context globals when supportModules: true. The root cause is clear — React 19's dev build calls performance.now() inside lazyInitializer, and Node.js 22 exposes performance as a built-in global, so this is a straightforward one-line addition.

What's Good

  • Correct fix: performance has been a Node.js built-in global since v16, so no compatibility risk given this repo targets Node 22.
  • Test quality improvement: Changing expect(result).toBeTruthy()expect(result).toBe('true') is a meaningful upgrade. The old form passes for any non-empty string — including 'false' — masking false positives.
  • Complete documentation: JSDoc, markdown docs, and CHANGELOG all updated consistently.
  • Follows the existing pattern: The inline comment in vm.ts reminds contributors to keep docs and JSDoc in sync when adding context globals (lines 217–219).

Minor Issue

The additionalContext tests (lines 76 and 87 in vm.test.ts) use the same runInVMexpect(result).toBeTruthy() pattern as the tests being fixed, but weren't updated to .toBe('true'). Not a blocker, but it leaves the same latent false-positive risk that this PR otherwise corrects.

Security / Performance

No concerns. performance is a read-only timing API; process is already exposed in this context so the sandbox boundary isn't meaningfully affected. Passing the host's performance object directly is the same pattern used for process, Buffer, etc.

Overall: LGTM — clean, minimal, well-tested fix.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 18, 2026

Code Review

Overview

Correct, minimal fix for a real React 19 dev-build regression. Adding performance to the VM context globals is the right approach, and the PR is well-scoped — it touches exactly what it needs to (the VM context, docs, JSDoc, changelog, and tests) without over-engineering.

Positives

  • Test strictness improvement: switching from .toBeTruthy() to .toBe('true') is a meaningful fix in its own right. The old assertions accepted "false" (a non-empty string is truthy), so the tests could have passed silently on a regression.
  • Regression test is well targeted: checking both typeof performance !== "undefined" and typeof performance.now === "function" covers the React 19 call site precisely.
  • Docs and JSDoc updated consistently with the code change — and the existing comment at vm.ts line 217 ("When adding anything to this object, update: 1. docs ... 2. configBuilder.ts") was followed.

Issues & Suggestions

1. Leftover .toBeTruthy() in the additionalContext tests (vm.test.ts:76, 87)
The same weak-assertion pattern the PR fixes in the Buffer / process / performance suite is still present in the additionalContext suite. Both 'typeof testString === "undefined"' and 'typeof testString !== "undefined"' evaluate to the string "true" or "false" in the VM, but .toBeTruthy() accepts either non-empty string. See inline comment.

2. performance is not stubbed when stubTimers: true
stubTimers replaces setTimeout / setInterval / etc. with no-ops (vm.ts:286–294) to make server rendering deterministic. performance.now() can also affect rendered output (e.g., unique keys, dev-only timing annotations). After this PR, when supportModules: true and stubTimers: true, real-clock performance.now() is exposed. This is consistent with the existing design — the host's process object is also shared — but it's worth documenting or considering a stub (e.g., { now: () => 0 }) to avoid non-determinism in SSR snapshots. Not a blocker, but a reasonable follow-up.

3. Shared host performance reference
The reference passed to the VM is the host process's singleton performance object. VM code can call performance.clearMarks(), performance.clearResourceTimings(), etc. and affect the host. This is the same trade-off already accepted for process, so it's consistent — just worth noting in a follow-up audit of what the VM sandbox actually isolates.

…rmance stubbing

Addresses follow-up review feedback on PR #3158:

- Swap `.toBeTruthy()` -> `.toBe('true')` in the `additionalContext`
  tests so a serialized `"false"` from `runInVM` no longer silently
  passes (same strictness improvement already applied to the
  Buffer / process / performance suite).
- Note in `js-configuration.md` that `performance` is the host's
  real object and is NOT stubbed by `stubTimers`, with guidance to
  override via `additionalContext` when strict SSR determinism is
  required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justin808
Copy link
Copy Markdown
Member Author

Follow-up review round addressed (I, Claude Code)

Addressed the three @claude[bot] OPTIONAL follow-ups from the latest review passes in commit 132780f.

Commit 132780ftest,docs: strengthen additionalContext assertions and document performance stubbing

  1. .toBeTruthy() leftover in additionalContext tests (packages/react-on-rails-pro-node-renderer/tests/vm.test.ts:76,87) — swapped both assertions to expect(result).toBe('true') so a serialized "false" from runInVM no longer slips past the test, matching the strictness fix already applied to the Buffer / process / performance suite in 8c630ea.
  2. performance not stubbed when stubTimers: true — documented in docs/oss/building-features/node-renderer/js-configuration.md. The reviewer offered two paths (document or stub); documenting is the conservative choice for this bug-fix PR because silently stubbing performance.now() to 0 could regress the exact React 19 React.lazy path this PR was opened to fix. The new note explains that performance is the host's real object, is not stubbed by stubTimers, and shows how to override via additionalContext ({ performance: { now: () => 0 } }) when strict SSR determinism is required.
  3. Shared host performance reference — no code change required. The reviewer flagged this as informational only ("consistent with how process works today… no action needed, just awareness"). The same trade-off is already documented for process and applies symmetrically here.

Verification: pnpm jest tests/vm.test.ts — 27/27 pass (including the updated additionalContext assertions). pnpm prettier --check clean on both touched files; pre-commit hooks (prettier, markdown-links, eslint) green.

All five inline review threads were already resolved in prior rounds; these three items were posted as general PR comments, so this summary serves as the response. No rationale replies needed on SKIPPED bot-summary / size-limit noise.

`supportModules: true` now injects `performance`, so the troubleshooting
doc's list of injected globals and its `additionalContext: { performance }`
workaround are stale. Updates the list and repoints the example at a
deterministic-stub override (the only remaining reason to set
`performance` via `additionalContext`).

Also extends the maintenance reminder comment in vm.ts to name all three
docs that need updating when a global is added, so the next contributor
doesn't miss rsc-troubleshooting.md the way this PR initially did.
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 18, 2026

This PR fixes a React 19 dev-build crash in the node renderer VM context by adding the host performance object to the default globals injected when supportModules: true. The fix is minimal and well-scoped.

POSITIVES

  • Minimal blast radius: guarded behind the existing supportModules flag, so it does not affect users who have not opted in.
  • Test quality improvement: converting expect(result).toBeTruthy() to expect(result).toBe('true') is a meaningful bug fix. Any non-empty string including the string 'false' is truthy in JavaScript, so the old assertions were not reliably verifying the VM expression result. The new form is strictly correct.
  • Docs are comprehensive: the note about performance not being stubbed by stubTimers and the additionalContext override pattern covers the main footgun.
  • Comment in vm.ts now lists all three files that need updating when new globals are added — a great maintenance guard.

ISSUES AND OBSERVATIONS

  1. Host performance object is mutable from within the VM

The injected performance is the real host Performance instance. VM code can call performance.mark(), performance.measure(), performance.clearMarks(), etc., mutating the host process performance timeline. This is consistent with how process is already exposed (it too is the real host object), so it is not a regression — but worth noting if a future audit of VM host-exposure happens.

  1. typeof performance.now === 'function' test is slightly incomplete

The PR verifies typeof performance.now === 'function' returns 'true'. This does not verify that calling performance.now() actually succeeds and returns a number. A check like typeof performance.now() === 'number' would replicate the exact React 19 failure path more faithfully.

  1. additionalContext override order is safe

additionalContext is applied after the supportModules block, so users who were previously working around this bug by injecting performance via additionalContext will continue to have their override honoured. Confirming the ordering is correct.

  1. rsc-troubleshooting.md example stub

The new { now: () => 0 } stub is the right recommendation now that performance is injected by default. A short inline comment (e.g., 'deterministic stub for byte-stable SSR') would clarify it is intentional, not an incomplete placeholder.

SUMMARY

Well-executed, low-risk fix. The one substantive suggestion is adding a performance.now() invocation test to verify the method is actually callable and returns a number, which would more faithfully replicate the React 19 failure mode.

result = await runInVM('typeof performance !== "undefined"', uploadedBundlePathForTest());
expect(result).toBe('true');

result = await runInVM('typeof performance.now === "function"', uploadedBundlePathForTest());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typeof performance.now === "function" check confirms the method exists, but doesn't verify it is actually invokable. Since the React 19 bug was performance.now() throwing at call time, consider adding one more assertion that exercises the call:

Suggested change
result = await runInVM('typeof performance.now === "function"', uploadedBundlePathForTest());
result = await runInVM('typeof performance.now === "function"', uploadedBundlePathForTest());
expect(result).toBe('true');
result = await runInVM('typeof performance.now() === "number"', uploadedBundlePathForTest());
expect(result).toBe('true');

// Add any globals that aren't in the default supportModules set.
// Example: override `performance` with a deterministic stub if rendered
// output embeds timing values and you need byte-stable SSR.
performance: { now: () => 0 },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stub is the right recommendation, but a brief comment on why would help readers understand this is deliberate:

Suggested change
performance: { now: () => 0 },
// Stub performance.now() with a constant so SSR output is byte-stable
// across renders (real performance.now() returns a different timestamp each call).
performance: { now: () => 0 },

URLSearchParams,
ReadableStream,
process,
performance,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for future reviewers: this is the real host Performance instance (same object as globalThis.performance in the Node.js process). VM code can call performance.mark(), performance.measure(), etc. and those writes will appear in the host's performance timeline. This is consistent with how process is already exposed, but worth documenting if the host-exposure surface is ever audited for isolation. If stricter isolation is needed, require('perf_hooks').performance creates a separate instance per require.

@justin808 justin808 merged commit b4b229f into main Apr 19, 2026
43 checks passed
@justin808 justin808 deleted the jg/3154-vm-performance branch April 19, 2026 09:31
justin808 added a commit that referenced this pull request Apr 23, 2026
…ons-docs

* origin/main:
  chore: remove redundant --rsc-pro install generator flag (#3105)
  ci: warn (don't fail) on Bencher main regression (#3168)
  test: enable RSpec --profile to surface slowest package tests (#3176)
  fix(node-renderer): expose performance in VM context when supportModules (#3158)
  docs: remove stale immediate_hydration references (#3139) (#3159)
  docs: restore absolute URL for node-renderer testing example (#3179)
  Bump Rspack dependencies to v2 (^2.0.0-0) (#3084)
  chore: remove obsolete webpack <5.106.0 pin (#3175)
  Move Node Renderer entry point to renderer/ directory (#3165)
  docs: address RSC pitfalls review follow-ups (#3155) (#3156)
  docs: remove fabricated DevConsole reference, link verified RSC tools (#2527) (#3163)

# Conflicts:
#	docs/oss/building-features/node-renderer/js-configuration.md
justin808 added a commit that referenced this pull request Apr 23, 2026
…ging' into jg/3122-rolling-deploy-adapter

* origin/jg/3122-unify-renderer-cache-staging: (39 commits)
  fix(specs): boot dummy specs without readline and drop redundant pnpm workspace (#3190)
  docs: add RSC migration success stories page (#1985) (#3162)
  Fix Bencher reporting permanently broken on pushes to main (#3148)
  docs: add example migrations guide (#3126)
  docs: remove defunct guavapass.com reference (#3199)
  chore: remove redundant --rsc-pro install generator flag (#3105)
  ci: warn (don't fail) on Bencher main regression (#3168)
  test: enable RSpec --profile to surface slowest package tests (#3176)
  fix(node-renderer): expose performance in VM context when supportModules (#3158)
  docs: remove stale immediate_hydration references (#3139) (#3159)
  docs: restore absolute URL for node-renderer testing example (#3179)
  Bump Rspack dependencies to v2 (^2.0.0-0) (#3084)
  chore: remove obsolete webpack <5.106.0 pin (#3175)
  Move Node Renderer entry point to renderer/ directory (#3165)
  docs: address RSC pitfalls review follow-ups (#3155) (#3156)
  docs: remove fabricated DevConsole reference, link verified RSC tools (#2527) (#3163)
  Scaffold CI workflow and build scripts for first-run consistency (#3097)
  Add OPTIONAL triage tier and fix recommendations to /address-review (#3161)
  chore: sync Gemfile.lock with term-ansicolor 1.11.3 (#3164)
  Simplify the docs sidebar and Pro landing pages (#3119)
  ...
justin808 added a commit that referenced this pull request Apr 23, 2026
* origin/main:
  fix(specs): boot dummy specs without readline and drop redundant pnpm workspace (#3190)
  docs: add RSC migration success stories page (#1985) (#3162)
  Fix Bencher reporting permanently broken on pushes to main (#3148)
  docs: add example migrations guide (#3126)
  docs: remove defunct guavapass.com reference (#3199)
  chore: remove redundant --rsc-pro install generator flag (#3105)
  ci: warn (don't fail) on Bencher main regression (#3168)
  test: enable RSpec --profile to surface slowest package tests (#3176)
  fix(node-renderer): expose performance in VM context when supportModules (#3158)
  docs: remove stale immediate_hydration references (#3139) (#3159)
  docs: restore absolute URL for node-renderer testing example (#3179)
  Bump Rspack dependencies to v2 (^2.0.0-0) (#3084)
  chore: remove obsolete webpack <5.106.0 pin (#3175)
  Move Node Renderer entry point to renderer/ directory (#3165)
  docs: address RSC pitfalls review follow-ups (#3155) (#3156)

# Conflicts:
#	CHANGELOG.md
justin808 added a commit that referenced this pull request Apr 29, 2026
Addresses the follow-up issues surfaced during review of #3158, where
validating a renderer fix locally required repo-specific workarounds.

Three independent breakages, one shared cause: the dummy app could make a
correct fix look broken or silently validate stale build artifacts.

1. `bin/dev` failed with `overlay.sockPort should be a number` because
   `bin/dev` exports `SHAKAPACKER_DEV_SERVER_PORT` as a string and
   Shakapacker passes it through unchanged on `devServer.port`. The Pro
   dummy's webpack development config forwarded that string straight to
   `@pmmmwh/react-refresh-webpack-plugin`, whose schema requires a number.
   Coerce with `Number()` (applied to both Pro dummy and execjs dummy).

2. Rails precompile hook failed with `cannot load such file -- ostruct`
   on Ruby 3.5+ because the Pro Gemfile was missing the explicit
   `ostruct`, `logger`, `benchmark` declarations the open-source Gemfile
   already had.

3. The dummy consumes the *built* `react-on-rails-pro-node-renderer/lib/`,
   not the TypeScript sources, so renderer fixes weren't exercised until
   the package was rebuilt. Add `node-renderer:fresh` and
   `node-renderer:build-watch` scripts, document the rebuild requirement
   inline in `Procfile.dev`, expand the Pro CLAUDE.md, and add a dedicated
   `validating-node-renderer-changes.md` checklist for reviewers.

Fixes #3177

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request Apr 30, 2026
Addresses the follow-up issues surfaced during review of #3158, where
validating a renderer fix locally required repo-specific workarounds.

Three independent breakages, one shared cause: the dummy app could make a
correct fix look broken or silently validate stale build artifacts.

1. `bin/dev` failed with `overlay.sockPort should be a number` because
   `bin/dev` exports `SHAKAPACKER_DEV_SERVER_PORT` as a string and
   Shakapacker passes it through unchanged on `devServer.port`. The Pro
   dummy's webpack development config forwarded that string straight to
   `@pmmmwh/react-refresh-webpack-plugin`, whose schema requires a number.
   Coerce with `Number()` (applied to both Pro dummy and execjs dummy).

2. Rails precompile hook failed with `cannot load such file -- ostruct`
   on Ruby 3.5+ because the Pro Gemfile was missing the explicit
   `ostruct`, `logger`, `benchmark` declarations the open-source Gemfile
   already had.

3. The dummy consumes the *built* `react-on-rails-pro-node-renderer/lib/`,
   not the TypeScript sources, so renderer fixes weren't exercised until
   the package was rebuilt. Add `node-renderer:fresh` and
   `node-renderer:build-watch` scripts, document the rebuild requirement
   inline in `Procfile.dev`, expand the Pro CLAUDE.md, and add a dedicated
   `validating-node-renderer-changes.md` checklist for reviewers.

Fixes #3177

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

React.lazy with streaming SSR and LimitChunkCountPlugin({ maxChunks: 1 })

1 participant