Turbopack: enable server HMR for app route handlers#91466
Merged
Conversation
Contributor
Tests Passed |
Merging this PR will improve performance by 3.38%
Performance Changes
Comparing Footnotes
|
aeb6fd2 to
abbe1d9
Compare
Contributor
Stats from current PR🔴 1 regression
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📝 Changed Files (8 files)Files with changes:
View diffsapp-route-ex..ntime.dev.jsDiff too large to display app-route-ex..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route.runtime.dev.jsDiff too large to display app-route.ru..time.prod.jsDiff too large to display 📎 Tarball URL |
b24f341 to
991945f
Compare
App router handlers are already built with the Turbopack runtime, so they can use server HMR. We also already get subscription events for their chunks. However, unlike app pages which use `__next_app__.require()` for dynamic devModuleCache lookup, route handlers capture userland exports statically in AppRouteRouteModule at construction time. This change makes routes behave a lot like pages, dynamically loading the user's code when requests are made. This way, we can freely invalidate and reload it when changes are made without evicting the entire require cache. This change removes the `isAppPage` restriction from `usesServerHmr`, extending server HMR coverage to all App Router entries (pages and route handlers) built with the Node.js runtime. For route handlers, we also clear the entry chunk from Node.js `require.cache` on each rebuild so the next `requirePage()` call re-executes the entry and obtains fresh module exports from `devModuleCache` (which HMR updates in-place). Test Plan: Added an additional e2e test, `with-dep`
40bf868 to
d6949f6
Compare
92b1dfe to
e819839
Compare
e819839 to
3e8bcd8
Compare
lukesandberg
approved these changes
Mar 19, 2026
wbinnssmith
added a commit
that referenced
this pull request
Mar 28, 2026
…responses Metadata routes (manifest.ts, robots.ts, sitemap.ts, icon.tsx, etc.) were incorrectly getting usesServerHmr=true after PR #91466 broadened it from app-page only to all app entries. This caused clearRequireCache() to skip deleting their chunks from require.cache and skip __next__clear_chunk_cache__(), so edits to manifest.ts (and other metadata routes) would not be reflected on the next request in Turbopack dev mode. Fix by adding an !isMetadataRoute(entryPage) guard so metadata routes always take the full cache-clearing path, while regular route handlers continue to benefit from server HMR. Co-Authored-By: Claude <noreply@anthropic.com>
wbinnssmith
added a commit
that referenced
this pull request
Mar 30, 2026
…responses Metadata routes (manifest.ts, robots.ts, sitemap.ts, icon.tsx, etc.) were incorrectly getting usesServerHmr=true after PR #91466 broadened it from app-page only to all app entries. This caused clearRequireCache() to skip deleting their chunks from require.cache and skip __next__clear_chunk_cache__(), so edits to manifest.ts (and other metadata routes) would not be reflected on the next request in Turbopack dev mode. Fix by adding an !isMetadataRoute(entryPage) guard so metadata routes always take the full cache-clearing path, while regular route handlers continue to benefit from server HMR. Co-Authored-By: Claude <noreply@anthropic.com>
wbinnssmith
added a commit
that referenced
this pull request
Mar 31, 2026
### What? Metadata routes (`manifest.ts`, `robots.ts`, `sitemap.ts`, `icon.tsx`, `apple-icon.tsx`, etc.) were not being hot-reloaded in Turbopack dev mode — changes to those files would not be reflected on subsequent requests until a full server restart. ### Why? PR #91466 extended `usesServerHmr = true` in `clearRequireCache()` (in `hot-reloader-turbopack.ts`) from `app-page` entries only to **all** `app`-type entries (pages + route handlers). The motivation was correct: regular route handlers like `app/api/hello/route.ts` use Turbopack's in-place module update model and benefit from server HMR. However, metadata routes (`/manifest.webmanifest/route`, `/robots.txt/route`, etc.) are also `app`-type entries but they are **not** suitable for in-place server HMR. When `usesServerHmr = true` for a metadata route, `clearRequireCache()` skips two critical invalidation steps: 1. Deleting the compiled chunk from `require.cache` 2. Calling `__next__clear_chunk_cache__()` Without those steps, the old module stays in-memory and all subsequent requests to `/manifest.webmanifest` (etc.) return the stale content. ### How? Added an `!isMetadataRoute(entryPage)` guard to the `usesServerHmr` expression in `clearRequireCache()`. This restores full cache invalidation for metadata routes on every rebuild while leaving regular route handler server HMR (added in #91466) intact. ```ts // Before const usesServerHmr = serverFastRefresh && entryType === 'app' && writtenEndpoint.type !== 'edge' // After const usesServerHmr = serverFastRefresh && entryType === 'app' && writtenEndpoint.type !== 'edge' && !isMetadataRoute(entryPage) // ← metadata routes always clear the cache ``` `isMetadataRoute('/manifest.webmanifest/route')` → `true` (excluded from server HMR) `isMetadataRoute('/api/hello/route')` → `false` (keeps server HMR, no regression) Also added a regression test: `metadata route hmr > reflects manifest.ts changes on fetch/refresh` in the `server-hmr` test suite, with a `manifest.ts` fixture that starts at `name: 'Version 0'`. The test patches the file and asserts the updated JSON is returned on the next fetch. Fixes #91981 --------- Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
wbinnssmith
added a commit
that referenced
this pull request
Mar 31, 2026
Rebase of #91318 on top of #91466. The lazy userland loading approach from #91318 (using a require() thunk instead of a static import) accurately captures devRequestTimingInternalsEnd — framework time ends when userland starts executing, not at module load time. Combined with the async getUserland() mechanism from #91466 for Turbopack dev HMR, which re-reads devModuleCache on every request so server HMR updates are picked up without re-executing the entry chunk. Co-Authored-By: Claude <noreply@anthropic.com>
wbinnssmith
added a commit
that referenced
this pull request
Mar 31, 2026
App router handlers are already built with the Turbopack runtime, so they can use server HMR. **We also already get subscription events for their chunks.** However, unlike app pages which use `__next_app__.require()` for dynamic devModuleCache lookup, route handlers capture userland exports statically in AppRouteRouteModule at construction time. This change makes routes behave a lot like pages, dynamically loading the user's code when requests are made. This way, we can freely invalidate and reload it when changes are made without evicting the entire require cache. Only `next dev` uses this lazy evaluation, as `next start` continues to eagerly import route handlers. This change removes the `isAppPage` restriction from `usesServerHmr`, extending server HMR coverage to all App Router entries (pages and route handlers) built with the Node.js runtime. For route handlers, we also clear the entry chunk from Node.js `require.cache` on each rebuild so the next `requirePage()` call re-executes the entry and obtains fresh module exports from `devModuleCache` (which HMR updates in-place). Test Plan: Added an additional e2e test, `with-dep`
wbinnssmith
added a commit
that referenced
this pull request
Mar 31, 2026
### What? Metadata routes (`manifest.ts`, `robots.ts`, `sitemap.ts`, `icon.tsx`, `apple-icon.tsx`, etc.) were not being hot-reloaded in Turbopack dev mode — changes to those files would not be reflected on subsequent requests until a full server restart. ### Why? PR #91466 extended `usesServerHmr = true` in `clearRequireCache()` (in `hot-reloader-turbopack.ts`) from `app-page` entries only to **all** `app`-type entries (pages + route handlers). The motivation was correct: regular route handlers like `app/api/hello/route.ts` use Turbopack's in-place module update model and benefit from server HMR. However, metadata routes (`/manifest.webmanifest/route`, `/robots.txt/route`, etc.) are also `app`-type entries but they are **not** suitable for in-place server HMR. When `usesServerHmr = true` for a metadata route, `clearRequireCache()` skips two critical invalidation steps: 1. Deleting the compiled chunk from `require.cache` 2. Calling `__next__clear_chunk_cache__()` Without those steps, the old module stays in-memory and all subsequent requests to `/manifest.webmanifest` (etc.) return the stale content. ### How? Added an `!isMetadataRoute(entryPage)` guard to the `usesServerHmr` expression in `clearRequireCache()`. This restores full cache invalidation for metadata routes on every rebuild while leaving regular route handler server HMR (added in #91466) intact. ```ts // Before const usesServerHmr = serverFastRefresh && entryType === 'app' && writtenEndpoint.type !== 'edge' // After const usesServerHmr = serverFastRefresh && entryType === 'app' && writtenEndpoint.type !== 'edge' && !isMetadataRoute(entryPage) // ← metadata routes always clear the cache ``` `isMetadataRoute('/manifest.webmanifest/route')` → `true` (excluded from server HMR) `isMetadataRoute('/api/hello/route')` → `false` (keeps server HMR, no regression) Also added a regression test: `metadata route hmr > reflects manifest.ts changes on fetch/refresh` in the `server-hmr` test suite, with a `manifest.ts` fixture that starts at `name: 'Version 0'`. The test patches the file and asserts the updated JSON is returned on the next fetch. Fixes #91981 --------- Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
App router handlers are already built with the Turbopack runtime, so they can use server HMR. We also already get subscription events for their chunks. However, unlike app pages which use
__next_app__.require()for dynamic devModuleCache lookup, route handlers capture userland exports statically in AppRouteRouteModule at construction time. This change makes routes behave a lot like pages, dynamically loading the user's code when requests are made. This way, we can freely invalidate and reload it when changes are made without evicting the entire require cache.Only
next devuses this lazy evaluation, asnext startcontinues to eagerly import route handlers.This change removes the
isAppPagerestriction fromusesServerHmr, extending server HMR coverage to all App Router entries (pages and route handlers) built with the Node.js runtime.For route handlers, we also clear the entry chunk from Node.js
require.cacheon each rebuild so the nextrequirePage()call re-executes the entry and obtains fresh module exports fromdevModuleCache(which HMR updates in-place).Test Plan: Added an additional e2e test,
with-dep