Skip to content

Add rspack migration with 7.6x faster builds#68

Open
AbanoubGhadban wants to merge 4 commits into
mainfrom
rspack-migration
Open

Add rspack migration with 7.6x faster builds#68
AbanoubGhadban wants to merge 4 commits into
mainfrom
rspack-migration

Conversation

@AbanoubGhadban
Copy link
Copy Markdown
Collaborator

Summary

  • Add rspack 2 config files (config/rspack/) mirroring the existing webpack config, with RSCRspackPlugin for RSC manifest generation
  • Add @rspack/core, @rspack/cli, @rspack/plugin-react-refresh, and rspack-manifest-plugin dependencies
  • Fix two missing optimization settings in shakapacker's generateRspackConfig(): splitChunks.chunks = 'all' and runtimeChunk = 'single'
  • Add Node.js 22.12.0 version files (required by @rspack/core v2)
  • Add bundle size measurement scripts (scripts/measure-*.sh)
  • Document all findings, fixes, and page status in RSPACK_MIGRATION.md

Build performance

Build Rspack Webpack Speedup
All 3 bundles (dev) 4.7s 36.2s 7.6x
Client only 3.7s ~33s ~9x
Server only 1.3s ~34s ~26x
RSC only 1.3s ~34s ~26x

Bundle size (production, gzip)

All RSC and SSR pages are at parity or slightly smaller than webpack. Example:

Page Rspack Webpack Diff
Product RSC 79 KB 80 KB -1%
Product Search RSC 76 KB 83 KB -9%
Product SSR 490 KB 494 KB -1%

RSCRspackPlugin bug fixes

Three bugs were found in react-on-rails-rsc's RSCRspackPlugin and fixed upstream in shakacode/react_on_rails_rsc#36:

  1. Server plugin skipped FS walkbeforeCompile only ran discovery for isServer: false
  2. Missing server manifest entries — client-only 'use client' files absent from server manifest caused createSSRManifest() to throw
  3. Server plugin skipped async import injection — production SSR got "Element type is invalid" errors because client files outside the server entry graph had wrong fallback IDs

Page status

Page Status
5 RSC pages All working (200, SSR, clean hydration)
4 SSR pages All working (200, SSR, clean hydration)
4 Client pages 500 — expected, @loadable/webpack-plugin not included (known rspack v2 incompatibility)

Verified in both development and production rspack builds with zero console errors.

How to switch bundlers

# Rspack
SHAKAPACKER_ASSETS_BUNDLER=rspack npx @rspack/cli build --config config/rspack/rspack.config.js

# Webpack (default, unchanged)
npx webpack --config config/webpack/webpack.config.js

Known issues

  1. @loadable/webpack-plugin not included — Client pages fail (rspack v2 incompatibility)
  2. __dirname mocked warnings from 4 .server.tsx files (harmless)
  3. Peer dep mismatch: shakapacker expects @rspack/core ^1.0.0, we use v2.0.3

Closes #64

Test plan

  • rspack build succeeds in dev mode (3 bundles, 4.7s)
  • rspack build succeeds in production mode (3 bundles, 3.6s)
  • All 5 RSC pages: HTTP 200, full SSR content, zero console errors/warnings
  • All 4 SSR pages: HTTP 200, full SSR content, zero console errors/warnings
  • Server manifest has 40/40 numeric module IDs (no string fallbacks)
  • Client manifest has 40/40 entries matching server manifest
  • Webpack build still works (no regressions)
  • Bundle sizes at parity with webpack

🤖 Generated with Claude Code

AbanoubGhadban and others added 4 commits May 24, 2026 21:00
Move both Ruby gems (react_on_rails, react_on_rails_pro) to the
feat/async-http-with-async-props branch and JS packages to the matching
react-on-rails-builds commit adcff279. The Pro gem's HTTP transport
migrates from HTTPX to async-http, so the httpx pin is no longer needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This initializer was a workaround for the HTTPX H2 pool-leak bug
(react_on_rails#3295). The async-http migration in this PR's gem version
makes HTTP clients request-scoped — no shared pool to leak — so the
workaround is no longer needed.

It is also incompatible with the new gem: it patches StreamRequest#initialize
with the old single-block signature, which now triggers
"wrong number of arguments (given 1, expected 0)" on the first streaming
render. Verified by running the app against the new gem and confirming
that every page (incl. /restaurant/:id/rsc, /product/rsc, /blog/rsc*) renders
cleanly with no SSR or hydration errors, and an idle-then-RSC request
(150s wait) completes without the old GOAWAY hang.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Includes rspack config files, bundle/asset measurement scripts,
migration documentation, and rspack npm dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@rspack/core v2 requires Node.js 22.12.0+. Adding .node-version and
.nvmrc so version managers (fnm, nvm, volta) pick up the requirement
automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Warning

Review limit reached

@AbanoubGhadban, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 30 minutes and 48 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 41b50522-5369-4d76-a698-b7a17479dfed

📥 Commits

Reviewing files that changed from the base of the PR and between 9d53948 and 4ea31ea.

⛔ Files ignored due to path filters (2)
  • Gemfile.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .node-version
  • .nvmrc
  • Gemfile
  • RSPACK_MIGRATION.md
  • config/initializers/rorp_stream_pool_leak_fix.rb
  • config/rspack/ServerClientOrBoth.js
  • config/rspack/clientRspackConfig.js
  • config/rspack/commonRspackConfig.js
  • config/rspack/development.js
  • config/rspack/production.js
  • config/rspack/rscRspackConfig.js
  • config/rspack/rspack.config.js
  • config/rspack/serverRspackConfig.js
  • config/rspack/test.js
  • package.json
  • scripts/measure-bundle-sizes.mjs
  • scripts/measure-gzip-assets.sh
  • scripts/measure-page-assets.sh
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rspack-migration

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR adds a complete Rspack 2 build configuration mirroring the existing webpack setup, achieving a 7.6x build speed improvement. It also switches the React on Rails gem dependencies to the feat/async-http-with-async-props branch (removing the now-unnecessary HTTPX stream-pool leak monkey-patch) and adds bundle-size measurement tooling.

  • config/rspack/: Nine new config files mirror the webpack multi-compiler setup — common base, client/server/RSC-specific configs, env-specific entrypoints, and a multi-compiler orchestrator. Applies two fixes for missing shakapacker defaults (splitChunks.chunks = 'all' and runtimeChunk = 'single'), and documents three upstream RSCRspackPlugin bugs fixed in the process.
  • package.json / pnpm-lock.yaml: Adds @rspack/core, @rspack/cli, @rspack/plugin-react-refresh, and rspack-manifest-plugin; switches react-on-rails-rsc to a local file:.yalc/… path (gitignored — see comment).
  • scripts/: Adds three measurement scripts (CDP-based and shell-based); the shell scripts contain a hard-coded absolute path to the author's machine (see comments).

Confidence Score: 3/5

The rspack config itself is well-structured and tested, but two issues prevent this from being runnable by any other contributor: the react-on-rails-rsc dependency points to a gitignored local yalc directory, and both measurement shell scripts embed an absolute path to the author's SSD.

The rspack configs, optimization fixes, and plugin wiring are solid and match the described test plan. However, package.json references react-on-rails-rsc as file:.yalc/react-on-rails-rsc — a directory that is explicitly listed in .gitignore and will not exist on any other machine — causing pnpm install to fail immediately after clone. The measurement scripts additionally hard-code an absolute SSD path, silently producing zeroes for every file check. These two issues need to be resolved before the branch is usable for CI or other team members.

package.json (broken yalc dependency), scripts/measure-gzip-assets.sh and scripts/measure-page-assets.sh (hard-coded absolute paths)

Important Files Changed

Filename Overview
package.json Adds rspack devDependencies and rspack-manifest-plugin; critical issue: react-on-rails-rsc points to a gitignored local yalc path that will break pnpm install for all contributors
scripts/measure-gzip-assets.sh New bundle-size measurement script; hard-codes an absolute path to the author's local SSD, causing silent zero-byte output for any other contributor
scripts/measure-page-assets.sh New page-asset measurement script; same hard-coded absolute PACKS_DIR path issue as measure-gzip-assets.sh
config/rspack/serverRspackConfig.js Server rspack config: strips CSS plugins, restricts to single chunk via LimitChunkCountPlugin, adds RSCRspackPlugin(isServer:true), targets Node; logic looks correct with two CSS-removal passes that are redundant but harmless
config/rspack/clientRspackConfig.js Client rspack config: fixes splitChunks.chunks and runtimeChunk, adds RSCRspackPlugin(isServer:false), injects postcss-loader; unused isHMR variable present
config/rspack/rscRspackConfig.js RSC bundle config: derives from server config with rscBundle=true (skips RSCRspackPlugin), adds WebpackLoader with enforce:'post', sets react-server condition names and aliases react-dom/server to false
config/rspack/ServerClientOrBoth.js Multi-compiler orchestration; correctly assembles three configs; log message for the default case inaccurately says 'both client and server' instead of all three
config/rspack/development.js Dev config: conditionally adds ReactRefreshPlugin when inliningCss is true; imports unused devServer binding
scripts/measure-bundle-sizes.mjs CDP-based bundle size measurement script using headless Chrome; well-structured but uses a static 5-second sleep instead of waiting for page load events, which could produce incomplete measurements on slow machines
Gemfile Switches react_on_rails and react_on_rails_pro from the upcoming-v16.3.0 branch to the feat/async-http-with-async-props feature branch, and removes the httpx gem in favour of async-http

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["rspack.config.js\n(entry point)"] --> B{"NODE_ENV"}
    B -->|development| C["development.js"]
    B -->|production| D["production.js"]
    B -->|test| E["test.js"]
    C --> F["ServerClientOrBoth(envSpecific)"]
    D --> F
    E --> F
    F --> G["commonRspackConfig()\ngenerateRspackConfig() + merge"]
    G --> H["clientRspackConfig()\nRSCRspackPlugin isServer:false\nsplitChunks.chunks=all\nruntimeChunk=single"]
    G --> I["serverRspackConfig()\nRSCRspackPlugin isServer:true\nLimitChunkCountPlugin maxChunks:1\ntarget:node"]
    G --> J["rscRspackConfig()\nserverRspackConfig(rscBundle=true)\nWebpackLoader enforce:post\nconditionNames:react-server"]
    F --> K{"Bundle env var?"}
    K -->|CLIENT_BUNDLE_ONLY| H
    K -->|SERVER_BUNDLE_ONLY| I
    K -->|RSC_BUNDLE_ONLY| J
    K -->|none| L["[clientConfig, serverConfig, rscConfig]"]
    H --> M["public/packs/\nclient bundle + manifests"]
    I --> N["ssr-generated/\nserver-bundle.js"]
    J --> O["ssr-generated/\nrsc-bundle.js"]
Loading

Reviews (1): Last reviewed commit: "Add Node.js 22.12.0 version files for rs..." | Re-trigger Greptile

Comment thread package.json
"react-on-rails-rsc": "github:shakacode/react_on_rails_rsc#update-rsc-19.0.3-fouc-runtime-chunk-fixes",
"react-on-rails-pro": "github:shakacode/react-on-rails-builds#adcff279edd06ad986d0122197abe9e932ed888a&path:react-on-rails-pro",
"react-on-rails-pro-node-renderer": "github:shakacode/react-on-rails-builds#adcff279edd06ad986d0122197abe9e932ed888a&path:react-on-rails-pro-node-renderer",
"react-on-rails-rsc": "file:.yalc/react-on-rails-rsc",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Local yalc path breaks pnpm install for all other contributors

file:.yalc/react-on-rails-rsc references a directory that is listed in .gitignore, so it is never committed to the repo. Anyone who clones this repository and runs pnpm install will get an error because .yalc/react-on-rails-rsc does not exist on their machine. The intent appears to be to land the upstream rspack-plugin bug fixes and then reference the published or tagged commit, but the yalc dev-loop reference slipped in. The specifier should be replaced with the upstream GitHub commit/branch once the fixes land (e.g. github:shakacode/react_on_rails_rsc#<sha>).


LABEL="${1:-unknown}"
BASE="http://localhost:3000"
PACKS_DIR="/mnt/ssd/shakacode-related/localhub-demo/public"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Hard-coded absolute path breaks the script for any machine other than the author's

PACKS_DIR is set to an absolute path that only exists on the developer's local SSD. Any other contributor who runs this script will silently get 0 KB for every file size because all the [ -f "$fpath" ] checks fail. The path should be derived dynamically from the repo root.

Suggested change
PACKS_DIR="/mnt/ssd/shakacode-related/localhub-demo/public"
PACKS_DIR="${PACKS_DIR:-$(git rev-parse --show-toplevel)/public}"


LABEL="${1:-unknown}"
BASE="http://localhost:3000"
PACKS_DIR="/mnt/ssd/shakacode-related/localhub-demo/public"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Same hard-coded absolute path as in measure-gzip-assets.sh — will silently produce zero-byte results on any machine that is not the original author's. Should be derived from the repo root dynamically.

Suggested change
PACKS_DIR="/mnt/ssd/shakacode-related/localhub-demo/public"
PACKS_DIR="${PACKS_DIR:-$(git rev-parse --show-toplevel)/public}"

console.log('[React on Rails] Creating only the RSC bundle (rspack).');
result = rscConfig;
} else {
console.log('[React on Rails] Creating both client and server bundles (rspack).');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Log message says "both client and server bundles" but actually exports three configs — client, server, and RSC. Misleading when tailing build logs.

Suggested change
console.log('[React on Rails] Creating both client and server bundles (rspack).');
console.log('[React on Rails] Creating all three bundles: client, server, and RSC (rspack).');

@@ -0,0 +1,20 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

const { devServer, inliningCss } = require('shakapacker/rspack');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unused importdevServer is destructured from shakapacker/rspack but never used in this file. The webpack equivalent uses it for sockPort, but the rspack ReactRefreshPlugin is instantiated with no options here.

Suggested change
const { devServer, inliningCss } = require('shakapacker/rspack');
const { inliningCss } = require('shakapacker/rspack');

Comment on lines +4 to +6
const isHMR = process.env.HMR;

const overrideCssModulesConfig = (config) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unused variableisHMR is declared but never referenced anywhere in this file. Carried over from the webpack equivalent (clientWebpackConfig.js) but never wired to any config path.

Suggested change
const isHMR = process.env.HMR;
const overrideCssModulesConfig = (config) => {
const overrideCssModulesConfig = (config) => {

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.

Rspack RSC: build the demo with Rspack using react-on-rails-rsc RspackPlugin

1 participant