Skip to content

Upgrade to Next.js 16.3 + migrate to App Router + adopt Cache Components - #8492

Open
aurorascharff wants to merge 43 commits into
reactjs:mainfrom
aurorascharff:upgrade-next-16
Open

Upgrade to Next.js 16.3 + migrate to App Router + adopt Cache Components#8492
aurorascharff wants to merge 43 commits into
reactjs:mainfrom
aurorascharff:upgrade-next-16

Conversation

@aurorascharff

@aurorascharff aurorascharff commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Upgrades react.dev to stable Next.js 16.3.0 and React 19.2.7, migrates from the Pages Router to the App Router, enables Cache Components, and refactors MDX rendering around Server Components.

Proofread preview: https://react-is6yt46wj-react-foundation.vercel.app/

Highlights

  • Migrates the full route tree to src/app, including persistent section layouts, errors and not-found boundaries, metadata, raw Markdown, and llms.txt.
  • Reads, compiles, and renders MDX on the server. Cached server-only helpers share page content and table-of-contents data without passing the full document through a Client Component.
  • Keeps page and section shells server-rendered while limiting client boundaries to interactive UI such as navigation, search, theme controls, and Sandpack.
  • Runs normal development and production builds on Turbopack, including a generated static Sandpack RSC worker instead of webpack-only raw-loader imports.
  • Fixes migration regressions affecting homepage first paint, font and theme hydration, internal links and fragment history, browser Back/Forward behavior around Sandpack iframes, and initial documentation-page layout.
  • Serves fonts from each deployment's own origin and consistently rejects development-only content in production HTML and Markdown routes.

This is an architecture migration, not a redesign. Existing content remains statically generated or partially prerendered; the production build generates 1,042 routes without instant = false opt-outs.

Mostly authored with agent assistance (Copilot agent mode, Claude Opus 4.7), guided by the canary next-cache-components-adoption skill.

Implementation details (AI-generated)

Routing and caching

  • Moved the route tree to src/app, with persistent layouts for Learn, Reference, and Community plus App Router error, not-found, metadata, and Route Handler conventions.
  • Added server-only cached helpers for reading source files, collecting paths, compiling MDX, and decoding errors. Page rendering and metadata generation reuse compiled content with cacheLife('max').
  • Uses async SectionPage and SectionContent Server Components for shared MDX loading and rendering.
  • Prerenders /api/md/[...path] with generateStaticParams and applies the same production visibility rules to HTML and raw Markdown routes.

MDX and component boundaries

  • Replaced client-side MDX deserialization with server-side @mdx-js/mdx compilation and rendering.
  • Extracts frontmatter, table-of-contents entries, language metadata, and layout wrappers while compiling on the server.
  • Keeps shared page and section layouts as Server Components. Client Components receive smaller serializable inputs and are reserved for interactions requiring browser state.
  • Imports HomeContent only from the homepage route, keeping it out of documentation route bundles while including the homepage in initial HTML.

Navigation and rendering

  • Replaced Pages Router APIs with next/navigation and uses Link from next/link for internal header, sidebar, MDX, table-of-contents, and fragment navigation.
  • Runs theme and platform initialization before paint; the root <html> suppresses the intentional class mismatch caused by that pre-hydration script.
  • Removed null Suspense holes from the documentation shell, preventing the sidebar from briefly rendering in the content column.
  • Prevents redundant Sandpack iframe src assignments from adding child-frame history entries that consume browser Back.
  • Preserves the Safari-only automatic scroll-restoration workaround used by the iOS edge-swipe Back gesture.
  • Uses same-origin font URLs and preloads so preview deployments do not wait on cross-origin production fonts.

Sandpack and Turbopack

  • Extracts Sandpack code blocks on the server and passes serializable file maps to client islands that lazy-load the interactive editor.
  • Builds and minifies the Sandpack RSC worker with esbuild, then publishes generated assets under public/sandpack-rsc; webpack-only raw-loader and metro-cache usage were removed.
  • Uses Turbopack for next dev and next build. Bundle analysis remains on webpack because the analyzer plugin is webpack-based.

Production parity

  • Restored scoped not-found layouts, Open Graph images, Tailwind source paths, analytics page views, and metadata previously supplied by _app, _document, and Seo.
  • Preserves legacy React error decoder links, including repeated args[] query parameters.
  • Updated the bundle-report workflow for App Router output while preserving the fork-safe pull-request comment flow.

Generated files

worker-bundle.dist.js and files under public/sandpack-rsc are generated by scripts/buildRscWorker.mjs. Their churn comes from rebuilding the worker for React 19.2 and Turbopack-compatible static delivery.

Deferred

  • eslint-config-next remains pinned to 14 for ESLint 7 compatibility. A full ESLint upgrade can move it to Next 16 separately.
Validation and performance testing

Build and static output

  • Clean Next.js 16.3 Turbopack production build completed in 41.7 seconds.
  • TypeScript passed and all 1,042 static, SSG, or partially prerendered routes were generated.
  • ESLint, Prettier, and git diff --check passed for the changed source.
  • Smoke-tested Home, Blog, Learn, Reference, Community, Versions, Warnings, unknown routes, and HTML/Markdown content parity.
  • Confirmed /learn/rsc-sandbox-test and /api/md/learn/rsc-sandbox-test remain development-only and return 404 in production.

Performance

  • Ran Lighthouse 13 mobile audits three times per origin and route, comparing Home, /learn/synchronizing-with-effects, and /reference/react/useState with live react.dev. Also ran desktop sanity audits and inspected filmstrip frames.
  • Verified FCP, LCP, Speed Index, TBT, CLS, transferred HTML/JS/CSS, request counts, and first-painted screenshots.
  • Confirmed the reported /reference/react-dom/components/input first-paint layout in five independent cold desktop loads; every first painted frame had the sidebar and article in their correct columns.
  • The current analyzer reports 166.63 KB gzip of global JavaScript, 920.92 KB gzip across the complete emitted JavaScript chunk pool, and 19.94 KB gzip of CSS. The workflow could not calculate a base delta because the stored base artifact used an older report format; the total chunk pool is not a per-page download.
  • Final simulated-mobile medians from three runs per route are below. Times are milliseconds; transferred HTML and JavaScript are compressed network bytes.
Route Site Score FCP LCP TBT CLS
Home Live 89 1,070 3,771 7 0
Home Preview 87 1,121 4,094 11 0
Effects Live 84 1,667 4,396 8 0.001
Effects Preview 82 1,668 4,743 38 0.001
useState Live 85 1,516 4,246 9 0
useState Preview 79 1,523 5,588 28 0
Route Live HTML Preview HTML Live JS Preview JS
Home 43.7 KB 50.0 KB 562.2 KB 675.7 KB
Effects 62.9 KB 75.4 KB 633.1 KB 742.7 KB
useState 54.6 KB 67.1 KB 633.0 KB 742.7 KB
  • The preview's additional HTML is expected from server-rendered MDX. It currently transfers about 110 KB more JavaScript on the tested documentation routes; despite that, median FCP is effectively at parity, CLS is at parity, and TBT remains below 40 ms. useState LCP remains the clearest performance item to watch.
  • Lighthouse attributed the earlier preview's layout shifts to cross-origin font swaps. After switching fonts to same-origin URLs, final deployed audits measured CLS of 0 on Home and useState, and 0.001 on Effects.

Hydration and Sandpack

  • Confirmed highlighted code tokens and Sandpack fallback markup are present in server HTML before JavaScript runs.
  • Delayed syntax-highlighter and Sandpack chunks by eight seconds: the full article and 41 visible code blocks remained rendered, DocSearch opened, and App Router navigation stayed client-side while those chunks were pending.
  • Confirmed Sandpack runtime chunks load after the initial route chunks rather than blocking the initial document render.

Navigation and compatibility

  • Tested internal route links, fragment links, direct deep links, reloads, 404s, and Back/Forward restoration.
  • Tested with an iPhone 13 WebKit profile: Safari received history.scrollRestoration = 'auto'; fragment navigation pushed history; Back restored the correct Effects article, fragment URL, and target position; Forward restored the next article.
  • Verified browser Back around pages containing Sandpack iframes and confirmed redundant iframe navigation no longer consumes the first Back action.
  • Verified error 31 on the preview and the historical reactjs.org/docs/error-decoder.html?... redirect. Repeated args[] values were preserved and the decoded message interpolated correctly.
  • The actual animated iOS edge-swipe snapshot still needs one physical-device check because Playwright WebKit cannot reproduce Safari's interactive swipe preview.

cc @icyJoseph

@aurorascharff
aurorascharff marked this pull request as ready for review June 22, 2026 22:49
Copilot AI review requested due to automatic review settings June 22, 2026 22:49
@aurorascharff
aurorascharff marked this pull request as draft June 22, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR upgrades the site to Next.js 16.3 canary + React 19.2 and migrates routing from the Pages Router to the App Router, enabling Cache Components and moving SEO/head logic to the Metadata API.

Changes:

  • Upgrade runtime/tooling (Next 16.3 canary, React 19.2, Node >=20.9, TS/ESLint adjustments) and update TS config/types.
  • Migrate content rendering to App Router routes under src/app/ with server-only helpers under src/lib/.
  • Replace the legacy <Seo>/Pages Router head setup with Metadata + root layout (src/app/layout.tsx) and add new client-side effects shims (analytics, scroll restoration).

Reviewed changes

Copilot reviewed 44 out of 51 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tsconfig.json Switch TS module resolution/JSX mode and include additional Next-generated type globs.
src/utils/compileMDX.ts Stop importing client-only MDX components by enumerating component names via a server-safe list; update return type to include languages.
src/types/jsx-bridge.d.ts Add React 19 JSX namespace bridge typings.
src/types/css.d.ts Add CSS module type shims (including @docsearch/css).
src/pages/errors/index.tsx Remove Pages Router error decoder index route.
src/pages/errors/[errorCode].tsx Remove Pages Router error decoder dynamic route + SSG logic.
src/pages/[[...markdownPath]].js Remove Pages Router catch-all MDX route + SSG path collection.
src/pages/_document.tsx Remove Pages Router custom document (head/scripts move to App Router layout).
src/pages/_app.tsx Remove Pages Router app wrapper (global CSS + effects move to App Router layout/client effects).
src/lib/readMarkdownPage.ts New server-only helper to read/compile MDX pages from src/content.
src/lib/loadErrorDecoderData.ts New server-only helper to load error code data + compile MDX for error decoder routes.
src/lib/collectPaths.ts New server-only helpers to enumerate content routes for static params generation.
src/lib/buildPageMetadata.ts New Metadata builder to replace legacy <Seo> behavior for canonical/alternates/OG/Twitter.
src/hooks/usePendingRoute.ts Remove route-transition pending-state tracking (no App Router router.events).
src/components/Search.tsx Migrate navigation to next/navigation and make Search a client component.
src/components/PageHeading.tsx Migrate router usage to usePathname.
src/components/MDX/MDXComponentsList.ts New server-safe list of MDX component names for MDX compilation.
src/components/MDX/MDXComponents.tsx Mark as client component; document syncing with MDXComponentsList.
src/components/MDX/ExpandableExample.tsx Replace next/router hash parsing with window.location.hash on client.
src/components/MDX/Challenges/Challenges.tsx Replace next/router usage with App Router primitives + hash handling.
src/components/Layout/useDeserializedMDX.tsx New client hook to deserialize MDX JSON into React nodes.
src/components/Layout/TopNav/TopNav.tsx Migrate routing to usePathname.
src/components/Layout/Sidebar/SidebarRouteTree.tsx Migrate routing to usePathname.
src/components/Layout/Page.tsx Convert to client component; remove <Seo>/next/head; accept pathname from server; render content with new MDX deserialization flow.
src/components/Layout/HomeContent.js Switch from custom use polyfill to React’s built-in use.
src/components/ErrorDecoderContext.tsx Update comments to reflect App Router/server component usage.
src/components/Seo.tsx Remove legacy SEO component in favor of Metadata API.
src/app/warnings/[slug]/page.tsx New App Router route for warnings pages (static params + metadata).
src/app/versions/page.tsx New App Router route for versions page (cache + metadata).
src/app/renderSectionPage.tsx New shared server renderer + metadata helper for sectioned MDX pages.
src/app/reference/[[...slug]]/page.tsx New App Router catch-all route for reference section.
src/app/page.tsx New App Router home route using server MDX read + metadata builder.
src/app/not-found.tsx New App Router not-found page using <Page> with explicit pathname/section.
src/app/llms.txt/route.ts Migrate llms.txt generator to App Router route handler (GET).
src/app/learn/[[...slug]]/page.tsx New App Router catch-all route for learn section.
src/app/layout.tsx New root layout: global CSS, viewport/metadata base, scripts, preloads, and client effects.
src/app/errors/page.tsx New App Router index route for error decoder.
src/app/errors/ErrorDecoderView.tsx New client view to deserialize/render error decoder MDX and provide context.
src/app/errors/[errorCode]/page.tsx New App Router dynamic error decoder route + static params + metadata.
src/app/error.tsx New global error boundary page for App Router.
src/app/DocsPage.tsx New client wrapper to feed deserialized MDX + toc into <Page>.
src/app/community/[[...slug]]/page.tsx New App Router catch-all route for community section.
src/app/clientEffects.tsx New client-only analytics + scroll restoration effects (replacing _app.tsx).
src/app/blog/[[...slug]]/page.tsx New App Router catch-all route for blog section.
src/app/api/md/[...path]/route.ts Migrate legacy API route to App Router route handler (GET).
package.json Update scripts for --webpack, adjust lint scripts, bump Next/React/types, and raise Node engine.
next.config.js Enable Cache Components + React compiler settings; add serverExternalPackages; keep webpack-based builds.
next-env.d.ts Update Next TypeScript env declarations (now includes .next type imports).
CLAUDE.md Update repo structure notes and add Next agent rules block.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/app/layout.tsx
Comment thread src/app/layout.tsx
Comment thread src/lib/readMarkdownPage.ts Outdated
Comment thread package.json Outdated
Comment thread src/components/Layout/Page.tsx
aurorascharff added a commit to aurorascharff/react.dev that referenced this pull request Jun 22, 2026
From Copilot's automated review on PR reactjs#8492:

- layout.tsx: render `<meta property="fb:app_id">` as a property tag
  directly in <head> (Next's `metadata.other` only emits name=).
- layout.tsx: restore the RSS autodiscovery <link> and the Algolia
  preconnect <link> that lived in the old Pages Router <Head>.
- readMarkdownPage.ts: switch readFileSync -> fs.promises.readFile to
  avoid blocking the event loop while compiling MDX.
- package.json: bump @types/node from ^14 to ^20 to match the new
  engines.node >=20.9.0.
- buildPageMetadata.ts + renderSectionPage.tsx + learn/blog
  generateMetadata: thread the section's routeTree through so we can
  re-emit the `algolia-search-order` meta tag on Learn pages and Blog
  posts (matches the old <Seo> behavior; Algolia uses it for ordering).
Runs `npx @next/codemod@canary upgrade canary`:

- next 15.1.12 → 16.3.0-canary.60
- react / react-dom ^19.0.0 → 19.2.7 (pinned)
- @types/react / @types/react-dom 19.2.x (pinned via resolutions)
- eslint-config-next 12.0.3 → 14 (last version supporting ESLint 7;
  16.x requires ESLint 8+ which is a separate upgrade)
- engines.node >=16.8.0 → >=20.9.0 (Next 16 minimum)

Type shims for React 19's stricter @types/react:

- src/types/jsx-bridge.d.ts re-exports React.JSX as the global JSX
  namespace used by existing Icon components
- src/types/css.d.ts provides ambient declarations for .css side-effect
  imports (needed under moduleResolution: 'bundler')
- @types/prop-types added (transitively required by legacy
  forwardRefWithAs.tsx)

Auto-updates from the codemod:

- tsconfig.json: moduleResolution 'node' → 'bundler', jsx 'preserve' →
  'react-jsx', .next/dev/types/** added to include
- next-env.d.ts: routes.d.ts and root-params.d.ts references
- CLAUDE.md: nextjs-agent-rules block injected by next dev (intended
  to be committed per the block's own instructions)
Migrates the docs site from the Pages Router to the App Router and
enables Cache Components (`experimental.cacheComponents` now top-level
`cacheComponents: true`).

Route tree (`src/app/`):

- `layout.tsx` — root layout (theme/uwu init script, fonts, GA, scroll
  restoration); replaces _document.tsx + _app.tsx
- `page.tsx` — home (`section: 'home'`)
- `{learn,reference,community,blog}/[[...slug]]/page.tsx` — section-
  specific catch-all docs pages (each owns its sidebar tree, section
  literal, and metadata generator)
- `warnings/[slug]/page.tsx`, `versions/page.tsx` — flat sections
- `errors/page.tsx`, `errors/[errorCode]/page.tsx` — error decoder
- `not-found.tsx`, `error.tsx` — error/404 boundaries
- `api/md/[...path]/route.ts`, `llms.txt/route.ts` — Route Handlers
  replacing the old API/static endpoints
- `renderSectionPage.tsx`, `DocsPage.tsx`, `clientEffects.tsx` —
  shared route helpers (server + client respectively)

Shared helpers (`src/lib/`, server-only):

- `readMarkdownPage.ts` — MDX read + compile from src/content
- `collectPaths.ts` — generateStaticParams enumeration
- `buildPageMetadata.ts` — canonical URL + hreflang alternates +
  open graph (replaces the old <Seo> component)
- `loadErrorDecoderData.ts` — error-codes fetch + MDX compile

Component-level changes:

- `next/router` → `next/navigation` across Page.tsx, Search.tsx,
  PageHeading.tsx, TopNav.tsx, SidebarRouteTree.tsx,
  ExpandableExample.tsx, Challenges.tsx
- `Page.tsx` takes `section` and `pathname` as props instead of
  sniffing them at runtime via `usePathname`; `<Seo>` and `<Head>`
  removed (replaced by App Router Metadata API)
- `MDXComponents.tsx` is now `'use client'` so the registry can host
  client-only components (Sandpack etc.); a parallel server-safe
  `MDXComponentsList.ts` keeps the component name list reachable from
  `compileMDX.ts`
- `useDeserializedMDX.tsx` — shared hook for revival of the
  serialized React tree on the client
- `usePendingRoute.ts` is a no-op (App Router has no equivalent of
  `router.events.routeChangeStart`; <Link> handles transitions)

next.config.js:

- `cacheComponents: true` and `reactCompiler: true` lifted out of
  `experimental`
- `serverExternalPackages` lists the Babel + MDX deps that compileMDX
  loads via runtime `require` (otherwise Next bundles them and breaks
  inside the cache scope)
- `turbopack: {}` placeholder so the build picks the webpack pipeline
  (the project's existing webpack config + Sandpack `raw-loader`
  imports aren't Turbopack-compatible yet)

Cache Components adoption:

- `'use cache'` on every page default export and `generateMetadata`
  (including `/errors/[errorCode]`)
- Zero `export const instant = false` opt-outs
- Build output: 816 routes, all Static or Partial Prerender, only
  `/api/md/[...path]` is dynamic

Other:

- `use(promise)` polyfill in HomeContent.js dropped in favour of
  React 19's built-in `use`
- `ErrorDecoderContext.tsx` doc comment refreshed to reflect that
  context now flows from a server component, not getStaticProps
- `worker-bundle.dist.js` regenerated by scripts/buildRscWorker.mjs
  with the updated React 19.2 runtime (much smaller)
From Copilot's automated review on PR reactjs#8492:

- layout.tsx: render `<meta property="fb:app_id">` as a property tag
  directly in <head> (Next's `metadata.other` only emits name=).
- layout.tsx: restore the RSS autodiscovery <link> and the Algolia
  preconnect <link> that lived in the old Pages Router <Head>.
- readMarkdownPage.ts: switch readFileSync -> fs.promises.readFile to
  avoid blocking the event loop while compiling MDX.
- package.json: bump @types/node from ^14 to ^20 to match the new
  engines.node >=20.9.0.
- buildPageMetadata.ts + renderSectionPage.tsx + learn/blog
  generateMetadata: thread the section's routeTree through so we can
  re-emit the `algolia-search-order` meta tag on Learn pages and Blog
  posts (matches the old <Seo> behavior; Algolia uses it for ordering).
The App Router migration and Cache Components (`cacheComponents`,
`'use cache'`) are all supported in stable 16.2.9 — the canary pin was
only needed during the migration itself. Builds clean with no config
warnings; all 816 routes still Static / Partial Prerender.

We removed every `instant = false` opt-out, so the 16.3-only `instant`
route config isn't used and there's nothing tying us to canary.
Per review feedback from @icyJoseph: instead of 'use cache' on every
page and generateMetadata, cache at the utility/data layer so callers
just work and the page render + generateMetadata share one compile.

- readMarkdownPage, collectSectionPaths, collectFlatSectionSlugs,
  loadErrorCodes, and a new compileErrorDecoderData helper are now
  'use cache' + cacheLife('max') (stable content, only changes on
  deploy; revalidate 30d instead of the default 15m).
- loadErrorDecoderData stays uncached and keeps the notFound() check,
  which can't run inside a 'use cache' scope; it delegates the cached
  compile to compileErrorDecoderData.
- Removed 'use cache' from all 9 routes and their generateMetadata.

Build unchanged: 816 routes, all Static / Partial Prerender.
@MaxwellCohen

Copy link
Copy Markdown
Contributor

@aurorascharff, I am so glad to see you talking up the challenge of moving to app router. I really hope that we can get this upgrade over the finish line.

I did some quick performance testing on the useActionState page (this pr build on my machine vs react.dev), and INP and LCP are the same, which is good. When I tested the previous attempt to use app router #8338 there was a slight decrease.

I am, however, still seeing an increase in JS sent to the browser (2.2 MB vs 2.0 MB) and used (1.1 MB vs 971 KB).

localhost JS coverage report from Chrome
image

Production JS coverage report from Chrome
image

Webpack build analysis shows a slight increase in client size bundles (920.36 KB gzip vs 880.62 KB gzip)

This PR
image

Main
image

On your maybe items

'use client' boundary on MDXComponents.tsx — there might be a cleaner split.

IMO MDXComponents.tsx should essentially be a barrel file for React.lazy, Next.js's dynamic components, or server components. This can greatly reduce the JavaScript bundle size. I created a proof-of-concept PR #8373 that can serve as a reference.

Per-section catch-alls vs one big catch-all — current shape avoids runtime section sniffing but means N near-identical page.tsx files.

The src/components/Layout/Page.tsx is really a mix-and-match of the diffrent layouts, so splitting that into the diffrent layouts.tsx and having the app router's page.tsx calls MDX render as a server component might be a good approach. I personally do not like catch-all routes because they result in JS-heavy pages and extra logic that I think should be handled by Next.js. Resulting in problems like currently, every page has all the home page info in the JS bundle

Overall, these changes make the site feel faster to navigate.

Per review feedback from @MaxwellCohen: <Page> is a shared client
component rendered by every route, and it statically imported
HomeContent (~2.7k LOC of homepage-only marketing/animation code).
That pulled the homepage into the shared client bundle of every docs,
reference and blog page.

Load it with next/dynamic instead. The homepage chunk (~116 KB) now
loads only at '/'; a representative docs route drops from ~941 KB to
~830 KB of client JS.
Per review feedback from @icyJoseph: the /api/md/[...path] handler that
serves raw markdown was Dynamic (a function per request). The content
set is fixed at build time, so prerender it instead.

- Add generateStaticParams (new collectAllContentPaths in collectPaths)
  enumerating every .md under src/content.
- 'use cache' + cacheLife('max') the file read (the dynamic/dynamicParams
  route segment configs are disallowed under cacheComponents).

Result: /api/md/[...path] is now SSG; +221 prerendered markdown
endpoints. Served from the CDN, so self-hosted clones stay static too.
Paths that match a section catch-all but have no backing .md file (e.g.
/learn/state, a sidebar header) were 500ing. The fs read threw inside
readMarkdownPage's 'use cache' scope, which surfaces as a render error
instead of falling through to notFound().

readMarkdownPage now returns PageData | null for a missing file instead
of throwing; callers decide notFound(). Removes the now-redundant
safeReadPage try/catch wrapper.
Expand the comment above the empty `turbopack: {}` to explain why the
build runs on webpack (custom webpack config + Sandpack raw-loader
imports aren't Turbopack-ready) and that it's a tracked follow-up.
@aurorascharff
aurorascharff marked this pull request as ready for review June 23, 2026 10:45
@aurorascharff

aurorascharff commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

@aurorascharff, I am so glad to see you talking up the challenge of moving to app router. I really hope that we can get this upgrade over the finish line.

I did some quick performance testing on the useActionState page (this pr build on my machine vs react.dev), and INP and LCP are the same, which is good. When I tested the previous attempt to use app router #8338 there was a slight decrease.

I am, however, still seeing an increase in JS sent to the browser (2.2 MB vs 2.0 MB) and used (1.1 MB vs 971 KB).

localhost JS coverage report from Chrome image

Production JS coverage report from Chrome image

Webpack build analysis shows a slight increase in client size bundles (920.36 KB gzip vs 880.62 KB gzip)

This PR image

Main image

On your maybe items

'use client' boundary on MDXComponents.tsx — there might be a cleaner split.

IMO MDXComponents.tsx should essentially be a barrel file for React.lazy, Next.js's dynamic components, or server components. This can greatly reduce the JavaScript bundle size. I created a proof-of-concept PR #8373 that can serve as a reference.

Per-section catch-alls vs one big catch-all — current shape avoids runtime section sniffing but means N near-identical page.tsx files.

The src/components/Layout/Page.tsx is really a mix-and-match of the diffrent layouts, so splitting that into the diffrent layouts.tsx and having the app router's page.tsx calls MDX render as a server component might be a good approach. I personally do not like catch-all routes because they result in JS-heavy pages and extra logic that I think should be handled by Next.js. Resulting in problems like currently, every page has all the home page info in the JS bundle

Overall, these changes make the site feel faster to navigate.

Thanks Maxwell! The +40 KB was from before I pushed a couple of follow-ups. I re-measured the useActionState page on both branches locally and it's basically at parity now, around 265 KB gzip on main vs 263 KB here. The fix was exactly what you spotted: the Page component statically imported HomeContent, so every page was shipping the homepage. It's dynamically imported now, so only the home route loads it.

On the architecture ideas, both are in the follow-ups section and I agree with the direction. I prototyped the MDXComponents barrel like in your POC: it works, but only saved about 7 KB, since the heavy part (CodeMirror) is already lazy-loaded inside SandpackRoot. The Page-to-layouts split is the bigger win and would also let MDX deserialize on the server, but it's a larger change, so I'd rather land the migration first and do that as a dedicated follow-up. What do you think?

@MaxwellCohen

Copy link
Copy Markdown
Contributor

Thank you for the performance updates. The lazy loading helps get JS downloaded to the same level as prod, great work!

image

@MaxwellCohen

Copy link
Copy Markdown
Contributor

Thanks Maxwell! The +40 KB was from before I pushed a couple of follow-ups. I re-measured the useActionState page on both branches locally and it's basically at parity now, around 265 KB gzip on main vs 263 KB here. The fix was exactly what you spotted: the Page component statically imported HomeContent, so every page was shipping the homepage. It's dynamically imported now, so only the home route loads it.

On the architecture ideas, both are in the follow-ups section and I agree with the direction. I prototyped the MDXComponents barrel like in your POC: it works, but only saved about 7 KB, since the heavy part (CodeMirror) is already lazy-loaded inside SandpackRoot. The Page-to-layouts split is the bigger win and would also let MDX deserialize on the server, but it's a larger change, so I'd rather land the migration first and do that as a dedicated follow-up. What do you think?

Very reasonable plan, at the end of the day, I want to see react.dev to be as fast as possible and showcase React's greatness both in content and implementation. There will be many changes because React and Next.js have evolved significantly over the last 3 years.

* Resize long og image text

* address feedback

(cherry picked from commit 7b6c3ce)
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.

3 participants