Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,23 @@ The goal is not to argue that every Inertia page should be replaced. The goal is
- A bounded `React on Rails Pro + RSC` dashboard slice can beat a matched `Inertia` control on navigation duration and `LCP` under a stricter alternating benchmark that balances route order.
- The current tradeoff is still real: under that balanced run, the `RSC` route is modestly slower on `responseEnd` and route-level controller timing.
- Route-scoped `Server-Timing` and an alternating comparison runner now make that tradeoff measurable instead of anecdotal.
- The custom Webpack and Rspack config now honors `SHAKAPACKER_DEV_SERVER_*` overrides the same way Ruby/Shakapacker does, so local verification can move off `3035` cleanly when another repo is already using it.
- GitHub-hosted demo validation now includes a real browser smoke pass for both comparison routes, not just build and controller-spec checks.

Latest balanced alternating local result on the reduced dashboard surface:

- Inertia navigation duration: `568.47ms`
- RSC navigation duration: `501.53ms`
- Inertia `LCP`: `602.00ms`
- RSC `LCP`: `525.00ms`
- Inertia `responseEnd`: `423.23ms`
- RSC `responseEnd`: `441.65ms`
- Inertia `action_total`: `250.50ms`
- RSC `action_total`: `278.32ms`
- Inertia navigation duration: `457.16ms`
- RSC navigation duration: `402.29ms`
- Inertia `LCP`: `501.00ms`
- RSC `LCP`: `421.00ms`
- Inertia `responseEnd`: `320.70ms`
- RSC `responseEnd`: `335.96ms`
- Inertia `action_total`: `163.10ms`
- RSC `action_total`: `169.74ms`

This alternating run is the stricter method because it rotates route order by cycle instead of relying on separate batches.
It keeps the user-visible win while preserving a measurable server-side tradeoff.
It is still the safer headline than the later 8-cycle clean-driver repeat, because that repeat surfaced one dev-asset outlier on the RSC route even though its medians stayed favorable.
This alternating run is the stricter method because it rotates route order by cycle instead of relying on separate batches, and this pass was rerun after fixing a local asset-proxy mismatch caused by another repo already listening on port `3035`.
It keeps the user-visible win on a longer `8`-cycle clean-port run while leaving only a modest server-side tradeoff instead of the larger gap seen in the first corrected rerun.
The earlier mixed-port 8-cycle clean-driver repeat is still useful as a diagnostic, but this corrected clean-port pass is the safer headline.

This is enough for a stronger positioning story.
It is still not enough for an upstream migration pitch or a production-performance claim.
Expand Down Expand Up @@ -94,11 +95,15 @@ These screenshots were captured from a signed-in local session on this branch.
`npm run setup && ./bin/shakapacker-dev-server`
`node client/node-renderer.cjs`
The Node renderer uses the local `devPassword` fallback only in `development` and `test`; set `RENDERER_PASSWORD` for production-like or hosted runs.
If port `3035` is already occupied by another local repo, start both Rails and the dev server with the same override, for example:
`SHAKAPACKER_DEV_SERVER_PORT=3036 bundle exec rails s -b 0.0.0.0 -p 3000`
`SHAKAPACKER_DEV_SERVER_PORT=3036 npm run setup && ./bin/shakapacker-dev-server`
4. Open the two demo routes and compare:
`/dashboard/inertia_demo`
`/dashboard/rsc_demo`
5. For the stricter benchmark method, run:
`ruby scripts/perf/compare_dashboard_routes.rb --base-url https://gumroad.dev --measure-base-url https://gumroad.dev --path /dashboard/inertia_demo --path /dashboard/rsc_demo --label dashboard-demo-alternating-4 --cycles 4 --server-warmup-requests 1 --require-driver-match`
For the longer headline-style local repeat, use the same command with `--cycles 8`.

If a long comparison run is interrupted after it writes per-run JSON files, rerun the same command with `--reuse-existing` to emit the final comparison summary without discarding completed samples.

Expand Down
6 changes: 2 additions & 4 deletions config/rspack/shakapacker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import fs from "fs";
import yaml from "js-yaml";
import { fileURLToPath } from "node:url";
import { getConfigEnvironment } from "./environment.js";
import { loadShakapackerConfig } from "../shakapackerConfig.js";

export default yaml.load(fs.readFileSync(fileURLToPath(import.meta.resolve("../shakapacker.yml"))))[getConfigEnvironment()];
export default loadShakapackerConfig(getConfigEnvironment());
44 changes: 44 additions & 0 deletions config/shakapackerConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from "fs";
import yaml from "js-yaml";
import { fileURLToPath } from "node:url";

const DEV_SERVER_ENV_PREFIX = "SHAKAPACKER_DEV_SERVER";
const BOOLEAN_VALUES = new Set(["true", "false"]);

const parseEnvValue = (value) => {
if (BOOLEAN_VALUES.has(value)) return value === "true";
if (/^-?\d+$/u.test(value)) return Number(value);
return value;
};

const applyDevServerEnvOverrides = (config) => {
if (!config?.dev_server) return config;

const devServer = { ...config.dev_server };
const envPrefix = devServer.env_prefix || DEV_SERVER_ENV_PREFIX;

for (const key of Object.keys(devServer)) {
const envValue = process.env[`${envPrefix}_${key.toUpperCase()}`];
if (envValue === undefined) continue;

if (key === "server" && typeof devServer.server === "object" && devServer.server !== null) {
devServer.server = {
...devServer.server,
type: parseEnvValue(envValue),
};
continue;
}

devServer[key] = parseEnvValue(envValue);
}

return {
...config,
dev_server: devServer,
};
};

export const loadShakapackerConfig = (environment) =>
applyDevServerEnvOverrides(
yaml.load(fs.readFileSync(fileURLToPath(import.meta.resolve("./shakapacker.yml"))))[environment],
);
8 changes: 2 additions & 6 deletions config/webpack/shakapacker.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import fs from "fs";
import yaml from "js-yaml";
import { fileURLToPath } from "node:url";
import { loadShakapackerConfig } from "../shakapackerConfig.js";

export default yaml.load(fs.readFileSync(fileURLToPath(import.meta.resolve("../shakapacker.yml"))))[
process.env.RAILS_ENV
];
export default loadShakapackerConfig(process.env.RAILS_ENV);
58 changes: 33 additions & 25 deletions docs/current-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The earlier review stack ([#1](https://github.com/shakacode/react-on-rails-demo-
- Added an alternating benchmark runner in `scripts/perf/compare_dashboard_routes.rb` so route order is balanced across cycles
- Added benchmark runner support for `--require-driver-match` and `--reuse-existing` so headline runs fail fast on browser-driver mismatch and long runs can be recovered without remeasuring completed samples
- Added median and `p95` primary-metric deltas plus slowest-pack-resource summaries to the alternating comparison output so dev-asset outliers are obvious without manual per-run digging
- Added a shared JS Shakapacker config loader so the custom Webpack and Rspack config honors `SHAKAPACKER_DEV_SERVER_*` overrides the same way Ruby/Shakapacker already does
- Installed Ruby gems locally
- Installed `node_modules` locally
- Brought up the Docker-backed local services
Expand All @@ -44,7 +45,7 @@ The earlier review stack ([#1](https://github.com/shakacode/react-on-rails-demo-
- Switched the local Shakapacker bundler path from Webpack to Rspack
- Added a real `config/rspack/` config tree instead of relying on the deprecated webpack-config fallback
- Confirmed `bin/shakapacker` builds successfully in both development and production with Rspack
- Confirmed `bin/shakapacker-dev-server` boots successfully on `https://gumroad.dev:3035/`
- Confirmed `bin/shakapacker-dev-server` boots successfully on its default `https://gumroad.dev:3035/` endpoint and on an overridden clean port for local verification
- Removed an obsolete `patch-package` patch for `react-dom@18.3.1` because React 19 already includes the needed `inert` support
- Added React on Rails Pro and the Node renderer configuration locally
- Added a dedicated `/dashboard/rsc_demo` route backed by the existing `CreatorHomePresenter`
Expand Down Expand Up @@ -109,7 +110,7 @@ Current local state:
- `node_modules` is installed
- gems are installed
- Rails boots locally on port `3000`
- the Rspack-backed Shakapacker dev server boots locally on port `3035`
- the Rspack-backed Shakapacker dev server boots locally on port `3035`, and the same setup now works on an overridden clean port such as `3036`
- `bin/dev` now boots the standalone React on Rails Pro Node Renderer on port `3800`
- local nginx now boots once `helperai.dev` cert files exist

Expand All @@ -122,7 +123,11 @@ That means the repository is now ready for comparison work on this machine.
- `make local` initially left nginx down because `docker/local-nginx/helperai_dev.crt` and `.key` were missing.
- The repository already includes `bin/generate_ssl_certificates` for this, but on macOS it may fail at `mkcert -install` if local sudo access is not available.
- For local-only boot, generating the `helperai.dev` cert files without installing the CA is sufficient to get nginx running, though browsers may still warn about trust.
- A later repeat with matching `Chrome 147` and `ChromeDriver 147` removed the earlier browser-driver mismatch, but one RSC dev-asset load still reported a `~19.3s` zero-transfer CSS duration, which is another reason the next pass needs production-like assets.
- A later manual verification pass found that another local repo's plain-HTTP webpack dev server had reclaimed port `3035`, which made Rails proxy `https://app.gumroad.dev/packs/...` asset requests to the wrong process and return `500`.
- The fix was to make the custom JS bundler config honor `SHAKAPACKER_DEV_SERVER_PORT`, then restart both Puma and `bin/shakapacker-dev-server` with the same override on a clean port such as `3036`.
- After that correction, both `/dashboard/inertia_demo` and `/dashboard/rsc_demo` rendered cleanly in a signed-in browser session again.
- An older mixed-port repeat with matching `Chrome 147` and `ChromeDriver 147` removed the earlier browser-driver mismatch, but one RSC dev-asset load still reported a `~19.3s` zero-transfer CSS duration.
- The corrected clean-port `8`-cycle repeat on `3036` did not reproduce that outlier, which makes the older run more useful as a diagnostic than as the current headline.

## Verified implementation state

Expand All @@ -132,15 +137,19 @@ That means the repository is now ready for comparison work on this machine.
Result: successful Rspack build with asset-size warnings but no compilation failures
- Dev server: `RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server`
Result: boots successfully on `https://gumroad.dev:3035/`
- Override-friendly dev server: `SHAKAPACKER_DEV_SERVER_PORT=3036 RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server`
Result: serves `https://gumroad.dev:3036/`, and Rails asset proxy requests succeed when Puma is started with the same `SHAKAPACKER_DEV_SERVER_PORT=3036`
- Standalone RSC build: `npm run build:rsc-demo`
Result: successful React on Rails Pro bundle build
- Manual browser verification: signed-in local session on `https://gumroad.dev/dashboard/inertia_demo` and `https://gumroad.dev/dashboard/rsc_demo`
Result: both routes render their expected demo headings after the clean-port override fix

## Current blocker for calling the branch "review ready"

The build path is working and the matched comparison surface is running, but two blockers remain before this is review ready as a persuasive stacked branch:

- React 19 adoption still exposes broad TypeScript cleanup work across the app.
- The strictest local result is still a development-mode measurement, and the matched-driver repeat exposed a dev-asset timing outlier while renderer-internal profiling is still missing.
- The strictest local result is still a development-mode measurement, and the older mixed-port matched-driver repeat exposed a dev-asset timing outlier while renderer-internal profiling is still missing.

Current `npx tsc --noEmit` results still show app-wide errors in categories like:

Expand All @@ -159,35 +168,34 @@ Short version:

- the `RSC` demo works end to end under React on Rails Pro
- the `Inertia` control works end to end on the same reduced data surface
- the `RSC` route now renders through the same `inertia` outer layout as the control so the comparison is cleaner
- the response-end pass shrank the raw RSC response to nearly match the Inertia control on transfer size
- earlier grouped batches overstated the RSC advantage because route order mattered
- under the balanced alternating method, the `RSC` route is still faster on total navigation duration and `LCP`
- under that same method, the Inertia control remains modestly faster on `responseEnd` and route-level `action_total`
- the position split shows the Inertia control is more sensitive to route order than the RSC route, which is useful context but not a reason to ignore the stricter aggregate result
- the corrected local setup now includes manual browser verification after fixing the asset-proxy mismatch caused by another repo using port `3035`
- the latest balanced alternating run was captured on that corrected setup with matching `Chrome 147` and `ChromeDriver 147`
- under that corrected method, the `RSC` route is still faster on total navigation duration and `LCP`
- under that same method, the Inertia control is still ahead on `responseEnd` and route-level server timing, but only modestly so on the longer clean-port repeat
- the current story is now favorable on user-visible metrics with a much smaller server-side tradeoff than the earlier mixed-port runs suggested

Useful numbers:

- alternating Inertia navigation duration: `568.47ms`
- alternating RSC navigation duration: `501.53ms`
- alternating Inertia LCP: `602.00ms`
- alternating RSC LCP: `525.00ms`
- alternating Inertia response end: `423.23ms`
- alternating RSC response end: `441.65ms`
- alternating Inertia `action_total`: `250.50ms`
- alternating RSC `action_total`: `278.32ms`
- alternating Inertia `compare_props`: `226.41ms`
- alternating RSC `compare_props`: `236.16ms`
- alternating Inertia HTML transfer: `14,240.5` bytes
- alternating Inertia navigation duration: `457.16ms`
- alternating RSC navigation duration: `402.29ms`
- alternating Inertia LCP: `501.00ms`
- alternating RSC LCP: `421.00ms`
- alternating Inertia response end: `320.70ms`
- alternating RSC response end: `335.96ms`
- alternating Inertia `action_total`: `163.10ms`
- alternating RSC `action_total`: `169.74ms`
- alternating Inertia `compare_props`: `144.80ms`
- alternating RSC `compare_props`: `145.49ms`
- alternating Inertia HTML transfer: `14,332.38` bytes
- alternating RSC HTML transfer: `15,265.0` bytes

So the current conclusion is:

- the comparison surface is real
- the user-visible win is now real on the matched surface
- the current stricter method still shows a real server-side tradeoff
- measurement order clearly matters, and the alternating runner now gives us a more defensible local result
- a later matched-driver repeat kept favorable medians for RSC, but it also exposed one dev-asset outlier that makes a production-like rerun the next real checkpoint
- the user-visible win is still real on the matched surface after correcting the local asset-proxy setup
- the current stricter method shows a real but modest server-side tradeoff on the longer clean-port repeat
- measurement order still matters, and the alternating runner remains the defensible local method
- the older mixed-port 8-cycle repeat is still a useful outlier-detection artifact, but the corrected clean-port 8-cycle run is the better current headline
- the performance pitch is promising, but not yet ready for upstream review

## Recommended next step
Expand Down
Loading
Loading