Skip to content

feat: upgrade to Next.js 15 + React 19#17781

Merged
wackerow merged 17 commits into
devfrom
next-15
Mar 19, 2026
Merged

feat: upgrade to Next.js 15 + React 19#17781
wackerow merged 17 commits into
devfrom
next-15

Conversation

@pettinarip
Copy link
Copy Markdown
Member

@pettinarip pettinarip commented Mar 16, 2026

Summary

Upgrade from Next.js 14.2 + React 18 to Next.js 15 + React 19.

Phase 1 (prep, still on Next 14)

  • Replace framer-motion with motion (27 files)
  • Update all Radix UI packages to latest
  • Add pnpm.overrides for React 19 peer deps

Phase 2 (the upgrade)

  • Bump next, react, react-dom, @types/react, eslint-config-next, @next/bundle-analyzer
  • Async params/searchParams migration across all pages
  • next.config.js cleanup: remove defaultConfig spread, remove experimental.instrumentationHook (stable in 15), move outputFileTracingExcludes to top-level, add staleTimes for client Router Cache
  • Split i18n/routing.ts into routing.ts + navigation.ts per next-intl official setup
  • Add "use client" to all ssr: false dynamic imports (Next 15 requirement)
  • React 19 type fixes: useRef requiring argument, ReactElement<unknown> props

Post-upgrade fixes

  • Replace htmr with html-react-parser — restores build-time type checking (removes ignoreBuildErrors workaround)
  • Rename server.tsxlazy.tsx for client-side dynamic import wrappers (naming consistency)
  • Add explicit force-cache to bare fetch() calls (Next 15 defaults to no-store)
  • Fix LanguagePicker: move button inside client component to avoid RSC + Radix Slot issue
  • Bump @netlify/plugin-nextjs to latest for Next 15 support

Deferred to follow-up PRs

  • prism-react-renderer v1→v2 rewrite
  • react-select upgrade
  • react-emoji-render replacement
  • Remove forwardRef (optional in React 19)
  • unstable_cacheuse cache
  • Turbopack adoption

Test plan

  • npx tsc --noEmit — zero type errors
  • pnpm lint — clean
  • pnpm build — successful
  • pnpm build-storybook — all stories render
  • Manual smoke test: homepage, content pages, wallet page, events search, developer tools, simulator, RTL language
  • Netlify preview deploy — verify function bundle sizes, Sentry, Matomo
  • Chromatic visual regression check

@github-actions github-actions Bot added config ⚙️ Changes to configuration files dependencies 📦 Changes related to project dependencies documentation 📖 Change or add documentation tooling 🔧 Changes related to tooling of the project labels Mar 16, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 16, 2026

Deploy Preview for ethereumorg ready!

Name Link
🔨 Latest commit 5f65555
🔍 Latest deploy log https://app.netlify.com/projects/ethereumorg/deploys/69bc3cc3de25c60008e5ebde
😎 Deploy Preview https://deploy-preview-17781.ethereum.it
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
7 paths audited
Performance: 59 (🟢 up 3 from production)
Accessibility: 94 (no change from production)
Best Practices: 100 (no change from production)
SEO: 99 (no change from production)
PWA: 59 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread app/[locale]/page.tsx Outdated
Comment thread app/[locale]/page.tsx Outdated
@pettinarip pettinarip marked this pull request as ready for review March 17, 2026 11:38
@pettinarip
Copy link
Copy Markdown
Member Author

html-react-parser Smoke Test Results

Compared server-rendered HTML between production (htmr) and preview deploy (html-react-parser) across multiple pages with dense <Translation> component usage.

All Translation-rendered content is identical.

Edge Case Page Result
<a> links inside text /en/stablecoins/ (4 Translation components with glossary links) ✅ Identical
<strong> tags /en/run-a-node/ ("starts with you") ✅ Identical
Nested <a> with href /en/run-a-node/ ("downloads a copy of the Ethereum blockchain") ✅ Identical
HTML entities (&#x27;, &amp;) /en/run-a-node/ (apostrophes, ampersands throughout) ✅ Identical
GlossaryTooltip transforms /en/stablecoins/ (dapps, cryptography, Ethereum account tooltips) ✅ Identical
Mixed text + HTML nodes /en/run-a-node/ (entire page content) ✅ Identical

Only differences found

  1. Dynamic data — market cap numbers fetched at different build times (expected)
  2. JSON-LD metadata — preview URLs (deploy-preview-17781.ethereum.it vs ethereum.org) and empty contributor arrays (expected for preview builds)
  3. One harmless <!-- --> comment — React empty text node marker in one stablecoins sentence (no visual impact)

Methodology

Fetched raw SSR HTML from both sites via curl, stripped tags/scripts, extracted visible text around known <Translation> component output, and diffed. Focused on pages with the highest density of Translation usage: /en/stablecoins/ (links, glossary tooltips), /en/run-a-node/ (links, strong tags, entities), and /en/what-is-ethereum/ (custom transform tags).

@pettinarip
Copy link
Copy Markdown
Member Author

Bundle Size Comparison (Next 14 vs Next 15)

Compared gzipped transfer sizes between production (Next.js 14 + React 18) and preview deploy (Next.js 15 + React 19) by fetching all JS chunks via curl.

Per-Page Initial Load

Page Production (Next 14) Preview (Next 15) Delta
Homepage / 800 KB (44 files) 834 KB (48 files) +34 KB (+4.3%)
Developers /en/developers/ 791 KB (45 files) 797 KB (48 files) +6 KB (+0.8%)
Find Wallet /en/wallets/find-wallet/ 854 KB (52 files) 842 KB (53 files) -12 KB (-1.4%)

Full Build Manifest (all shared chunks across the entire app)

Production (Next 14) Preview (Next 15) Delta
All manifest chunks 735 KB (39 files) 776 KB (43 files) +41 KB (+5.6%)

Biggest Chunks Breakdown

Category Production Preview Notes
Largest single chunk 95 KB 114 KB Larger shared framework chunk
Framework chunks (top 3) 95+77+54 = 226 KB 114+78+54 = 246 KB +20 KB — React 19 + Next 15 runtime
Polyfills 37 KB 37 KB Identical
Webpack runtime 3 KB 4 KB +1 KB

Key Takeaways

  • Overall increase is ~41 KB (+5.6%) across the full shared bundle — reasonable for a major version bump of both React and Next.js
  • The increase is concentrated in framework/runtime chunks, not application code. The +20 KB in the top 3 chunks is React 19's slightly larger runtime
  • Page-specific code is comparable or smaller — find-wallet actually shrank by 12 KB, suggesting motion (replacing framer-motion) and updated Radix packages are leaner for interactive-heavy pages
  • More granular chunking — Next 15 produces 43 shared chunks vs 39, meaning better code splitting and potentially better caching on navigation between pages
  • No red flags — no single chunk has ballooned unexpectedly

Methodology

Fetched homepage HTML from both sites, extracted all /_next/static/chunks/*.js URLs, then fetched each chunk with curl --compressed and measured size_download (gzipped transfer size). For the full manifest comparison, fetched _buildManifest.js from both builds and downloaded all referenced chunks.

Copy link
Copy Markdown
Collaborator

@myelinated-wackerow myelinated-wackerow left a comment

Choose a reason for hiding this comment

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

Review: Next.js 15 + React 19 Upgrade

Really clean upgrade, @pettinarip. The phased approach is solid and the patterns are consistent across all 143 files. Async params migration, "use client" on lazy imports, framer-motion -> motion, i18n routing split, LanguagePicker RSC fix, React 19 type fixes -- all look good.

Two things to address:

  1. force-cache on GasTable fetches needs revalidation -- GasTable.tsx fetches gas prices and ETH price from Etherscan with { cache: "force-cache" }, and the page has no revalidate export. This means users see build-time prices until the next deploy. Since what-is-ether is a static React page (no public/content/ dependency), it's safe to use { next: { revalidate: N } } here instead. The exact interval is flexible -- this isn't a live gas tracker, but we don't want it running too stale either. 300 (5 min), 1800 (30 min), or 3600 (1 hr) would all be reasonable -- your call. Same consideration applies to the collectibles page fetches for badges/stats.

  2. Document staleTimes rationale -- The staleTimes: { dynamic: 30, static: 180 } restores Next 14 client Router Cache behavior, but the specific values (especially static at 180 vs Next 14's 300) would benefit from a brief comment explaining the intent for future maintainers. Something like:

    // Restore client-side Router Cache durations closer to Next 14 defaults
    // (Next 15 changed dynamic to 0s; static was 300s in Next 14)
    staleTimes: { dynamic: 30, static: 180 },

Reviewed by Claude Opus 4.6 (1M context)

@pettinarip
Copy link
Copy Markdown
Member Author

@wackerow thanks.
re: force-cache on GasTable, this isn't a regression. force-cache was already the default fetch behavior in Next 14. Making it explicit here just preserves parity. Improving revalidation for gas prices is a valid idea but would consider out of scope for this upgrade PR.

re: point 2, I thought that those numbers that I put were the v14 defaults. I'll update them again to match v14.

Copy link
Copy Markdown
Member

@wackerow wackerow left a comment

Choose a reason for hiding this comment

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

let's go! 🎉

@wackerow wackerow merged commit 6108872 into dev Mar 19, 2026
8 checks passed
@wackerow wackerow deleted the next-15 branch March 19, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config ⚙️ Changes to configuration files dependencies 📦 Changes related to project dependencies documentation 📖 Change or add documentation tooling 🔧 Changes related to tooling of the project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants