fix(app-router): exclude dev overlay from production client bundle#2284
Conversation
commit: |
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
/bigbonk review for issues and edge cases |
There was a problem hiding this comment.
Reviewed the change. The fix is correct and well-scoped.
What the bug was: the App Router browser entry statically imported ./dev-error-overlay.js at the top of the module, so the ~1800-line dev overlay (plus its react-dom/client createRoot usage) was pulled into the production client bundle even though it was only referenced inside if (import.meta.env.DEV) / if (import.meta.hot) blocks. import.meta.hot alone is only undefined at runtime in prod — it does not let the bundler statically dead-code-eliminate the block, and the static import kept the module alive regardless.
Why this fix works:
- The top-level static import is replaced with a
typeof import(...)type-only import (erased at compile time) plus dynamicawait import()behindimport.meta.env.DEV. - Changing the HMR guard from
if (import.meta.hot)toif (import.meta.env.DEV && import.meta.hot)(line 2115) is the key bit:import.meta.env.DEVis statically replaced withfalsein prod, so Rolldown can eliminate the whole block, including the"rsc:update"literal and the second dynamic import of the overlay. - The
null-fallback inbootstrapHydrationcorrectly routes to the prodonCaughtError/onRecoverableErroroptions.
Parity: This now matches the pattern already used by the Pages Router client entry (pages-client-entry.ts:173), so the two entries are consistent. No Cloudflare worker entry imports the overlay, so nothing else is affected.
Verification: vp test run tests/app-router-production-build.test.ts passes (9/9) and vp check on the modified file is clean.
One non-blocking nit inline.
| // re-populates it. Without this, an old "DropZone is not defined" | ||
| // error would linger after the developer fixed the bug. | ||
| dismissOverlay(); | ||
| const devErrorOverlay = await import("./dev-error-overlay.js"); |
There was a problem hiding this comment.
Non-blocking: this is a second dynamic import("./dev-error-overlay.js") site. It's harmless (the module is already loaded in main() at line 1397 and the import resolves from cache, and installDevErrorOverlay is idempotent via its installed guard), so functionally fine. Just noting it reads a little redundantly versus the eager load in main(). Not worth changing unless you want to thread the already-loaded reference through.
|
Review posted successfully. Here's my summary: Review: PR #2284 — exclude dev overlay from production client bundleVerdict: Correct and well-scoped. Approving with one non-blocking nit. What the bug wasThe App Router browser entry statically imported Why the fix is right
Verification I ran
Non-blocking nitLine 2167 introduces a second dynamic import of the same module. It's harmless (cached, and No correctness, edge-case, or parity issues found that warrant blocking. |
|
/bigbonk re-review |
There was a problem hiding this comment.
Re-review (/bigbonk)
Verdict: Correct, well-scoped, and the prior nit was addressed. Approving.
What the fix does
The App Router browser entry (app-browser-entry.ts) previously had a top-level static import of ./dev-error-overlay.js. Even though the overlay was only used inside import.meta.env.DEV / import.meta.hot guards, a static import keeps the ~1800-line module (and its react-dom/client createRoot usage) alive in the production client bundle — import.meta.hot is merely undefined at runtime in prod and does not enable static DCE.
The fix:
- Replaces the static import with a type-only
typeof import(...)(erased) plus a single dynamicawait import("./dev-error-overlay.js")behindimport.meta.env.DEV(line 1397). - Threads the loaded module reference through
bootstrapHydration(rscStream, devErrorOverlay)and into the HMR closure (devErrorOverlay?.dismissOverlay()at line 2167). - Tightens the HMR guard at line 2115 from
if (import.meta.hot)toif (import.meta.env.DEV && import.meta.hot), which is the key change that lets Rolldown statically eliminate the whole HMR block (including the"rsc:update"literal at line 2197 and theapplyRscHmrUpdatebody) in prod. - The hydrate-root options branch now keys on
import.meta.env.DEV && devErrorOverlay, falling back to the prodonCaughtError/onRecoverableErrorpath.
Prior nit resolved
The previous review flagged a second dynamic import of the overlay inside the HMR handler. That is no longer present — line 2167 uses the threaded devErrorOverlay?.dismissOverlay() reference. There is now exactly one import("./dev-error-overlay.js") site (confirmed via grep).
Verification I ran
vp test run tests/app-router-production-build.test.ts— 9/9 pass (47s)vp check packages/vinext/src/server/app-browser-entry.ts— clean (format, lint, types)- Confirmed
"rsc:update"anddev-error-overlayliterals only appear in the dev-gated path / source comments (stripped by minification), so the new test needles are meaningful regression guards. - Confirmed parity with the established Pages Router pattern at
pages-client-entry.ts:173-181— both entries now dynamic-import the overlay behindimport.meta.env.DEV. - No Cloudflare worker entry imports the overlay, so nothing else is affected.
No correctness, edge-case, or parity issues found.
|
Re-review posted and approved. SummaryThis PR correctly fixes #1724 by excluding the App Router dev error overlay from the production client bundle. Verified:
Tests run:
No correctness, edge-case, or parity issues found. Approved. |
Summary
Verification
Close #1724