Migrazione a Next.js 16: Performance, Turbopack e React 19
Come abbiamo migrato THEJORD a Next.js 16: architettura App Router, Turbopack (7x più veloce), React 19 Server Components, best practices e performance improvements ottenuti.
Migrazione a Next.js 16: Performance, Turbopack e React 19
THEJORD.IT è stato costruito con le tecnologie più moderne per garantire performance eccezionali e developer experience ottimale. In questo articolo tecnico raccontiamo il processo di migrazione a Next.js 16, le sfide affrontate, i benefici ottenuti e le lezioni apprese.
Perché Next.js 16
La decisione di adottare Next.js 16 (con React 19 e Turbopack) è stata guidata da obiettivi chiari:
- Performance: Build time ridotto del 70%, hot reload istantaneo
- Developer Experience: Turbopack elimina lentezza di Webpack in dev mode
- React 19 Features: Server Components, Actions, Suspense migliorato
- SEO: Server-Side Rendering (SSR) e Static Site Generation (SSG) built-in
- Type Safety: Integrazione TypeScript nativa first-class
Cosa Cambia rispetto a Next.js 14
| Feature | Next.js 14 | Next.js 16 |
|---|---|---|
| Bundler | Webpack (lento) | Turbopack (10x più veloce) |
| React Version | React 18 | React 19 (Actions, Suspense++) |
| Build Time | ~45s (THEJORD) | ~12s (THEJORD) |
| Hot Reload | 1-3s | <200ms |
| Server Actions | Experimental | Stable |
| Partial Prerendering | ❌ | ✅ (preview) |
Architettura di THEJORD con Next.js 16
App Router (app/) vs Pages Router
THEJORD utilizza la nuova App Router introdotta in Next.js 13 e perfezionata in 16:
thejord-web/
├── app/
│ ├── [locale]/ # i18n routing
│ │ ├── layout.tsx # Root layout (shared)
│ │ ├── page.tsx # Homepage
│ │ ├── tools/
│ │ │ ├── diff-checker/
│ │ │ │ └── page.tsx # Diff Checker tool
│ │ │ ├── hash-generator/
│ │ │ │ └── page.tsx # Hash Generator tool
│ │ │ └── ...
│ │ ├── blog/
│ │ │ ├── page.tsx # Blog list (SSG)
│ │ │ └── [slug]/
│ │ │ └── page.tsx # Blog post (SSG)
│ │ └── admin/
│ │ └── posts/
│ │ └── page.tsx # Admin panel (CSR)
│ └── api/
│ ├── proxy/ # API proxy to backend
│ └── revalidate/ # ISR revalidation
├── components/ # React components
├── lib/ # Utilities
└── public/ # Static assets
Rendering Strategies
THEJORD combina diverse strategie per ottimizzare performance e SEO:
1. Static Site Generation (SSG) - Blog Posts
// app/[locale]/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return (
{post.title}
)
}
// Metadata SEO generata a build time
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return {
title: post.metaTitle,
description: post.metaDescription,
}
}
Benefici: HTML pre-renderizzato, instant loading, perfect SEO
2. Incremental Static Regeneration (ISR) - Tool Pages
// app/[locale]/tools/diff-checker/page.tsx
export const revalidate = 3600 // Revalidate ogni ora
export default function DiffChecker() {
return
}
// API per forzare revalidation
// POST /api/revalidate?path=/tools/diff-checker
export async function POST(request: Request) {
const { path } = await request.json()
await revalidate(path)
return Response.json({ revalidated: true })
}
Benefici: Static speed + dynamic freshness
3. Client-Side Rendering (CSR) - Admin Panel
// app/[locale]/admin/posts/page.tsx
'use client'
import { useState, useEffect } from 'react'
export default function AdminPosts() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/proxy/api/posts')
.then(res => res.json())
.then(setPosts)
}, [])
return
}
Benefici: Interattività, real-time updates, protected routes
Turbopack: Il Nuovo Bundler
Webpack vs Turbopack: Benchmark Reali
Test eseguiti su THEJORD codebase (45 componenti, 120 file):
| Operazione | Webpack (Next 14) | Turbopack (Next 16) | Miglioramento |
|---|---|---|---|
| Cold start | 8.2s | 1.1s | 7.5x più veloce |
| Hot reload (small change) | 2.1s | 0.15s | 14x più veloce |
| Production build | 44s | 12s | 3.7x più veloce |
| Memory usage (dev) | 850MB | 420MB | -50% |
Come Abilitare Turbopack
// next.config.mjs
const nextConfig = {
// Dev mode con Turbopack
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
}
export default nextConfig
# package.json
{
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start"
}
}
Gotchas e Limitazioni
- Custom Webpack config: Non completamente supportata, richiede migration
- Some loaders: Potrebbero non funzionare (es. vecchi Webpack loaders)
- Source maps: Formato leggermente diverso, alcuni debugger potrebbero avere issue
React 19 Server Components
Quando Usare Server Components
Server Components eseguono sul server, riducono JavaScript inviato al client:
// components/BlogPostList.tsx (Server Component)
async function BlogPostList() {
// Fetch diretto da database, senza API call client-side
const posts = await db.posts.findMany({
where: { published: true },
orderBy: { publishedAt: 'desc' }
})
return (
{posts.map(post => (
))}
)
}
// Nessun 'use client' → Server Component di default
Benefici:
- Zero JavaScript client-side per rendering
- Accesso diretto a database/filesystem
- Nessun waterfall di API calls
- Bundle size ridotto
Quando Usare Client Components
// components/DiffChecker.tsx (Client Component)
'use client'
import { useState } from 'react'
import { diffLines } from 'diff'
export function DiffChecker() {
const [text1, setText1] = useState('')
const [text2, setText2] = useState('')
const [diff, setDiff] = useState([])
const handleCompare = () => {
const result = diffLines(text1, text2)
setDiff(result)
}
return (
// UI interattiva con state, events, browser APIs
)
}
Usa Client Components per:
- State (useState, useReducer)
- Effects (useEffect, custom hooks)
- Event handlers (onClick, onChange)
- Browser APIs (localStorage, Web Workers)
- Third-party interactive libraries
Composition Pattern
// app/[locale]/tools/diff-checker/page.tsx (Server Component)
import { DiffChecker } from '@/components/DiffChecker'
import { ToolMetadata } from '@/components/ToolMetadata'
export default async function DiffCheckerPage() {
// Fetch metadata sul server
const metadata = await getToolMetadata('diff-checker')
return (
{/* Server Component */}
{/* Client Component passato come children */}
)
}
React 19 Server Actions
Server Actions permettono di chiamare funzioni server da client senza creare API route:
// app/actions.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const post = await db.posts.create({
data: { title, content, published: false }
})
revalidatePath('/admin/posts')
return { success: true, postId: post.id }
}
export async function publishPost(postId: string) {
await db.posts.update({
where: { id: postId },
data: { published: true, publishedAt: new Date() }
})
revalidatePath('/blog')
return { success: true }
}
// components/CreatePostForm.tsx
'use client'
import { createPost } from '@/app/actions'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
)
}
export function CreatePostForm() {
return (
)
}
Benefici:
- No API route boilerplate
- Type-safe client-server communication
- Progressive enhancement (funziona senza JS)
- Automatic revalidation
Migrazione Step-by-Step
1. Preparazione
# Backup codebase
git checkout -b migration-nextjs-16
# Update dependencies
npm install next@latest react@latest react-dom@latest
npm install --save-dev @types/react@latest @types/react-dom@latest
2. Migrare a App Router
# Struttura vecchia (pages/)
pages/
├── index.tsx
├── tools/
│ └── diff-checker.tsx
└── blog/
└── [slug].tsx
# Nuova struttura (app/)
app/
├── page.tsx # pages/index.tsx
├── tools/
│ └── diff-checker/
│ └── page.tsx # pages/tools/diff-checker.tsx
└── blog/
└── [slug]/
└── page.tsx # pages/blog/[slug].tsx
3. Convertire getStaticProps → Server Components
// ❌ Vecchio (pages/)
export async function getStaticProps() {
const posts = await getPosts()
return { props: { posts } }
}
export default function Blog({ posts }) {
return
}
// ✅ Nuovo (app/)
export default async function Blog() {
const posts = await getPosts() // Fetch diretto
return
}
4. Migrare API Routes
// pages/api/posts.ts → app/api/posts/route.ts
// ❌ Vecchio
export default async function handler(req, res) {
if (req.method === 'GET') {
const posts = await getPosts()
res.json(posts)
}
}
// ✅ Nuovo
export async function GET() {
const posts = await getPosts()
return Response.json(posts)
}
export async function POST(request: Request) {
const body = await request.json()
const post = await createPost(body)
return Response.json(post, { status: 201 })
}
5. Update next.config.js
// next.config.mjs
const nextConfig = {
// Abilita Turbopack in dev
experimental: {
turbo: {},
},
// i18n routing
i18n: {
locales: ['it', 'en'],
defaultLocale: 'it',
},
// Image optimization
images: {
domains: ['thejord.it'],
},
}
export default nextConfig
Performance Improvements Ottenuti
Lighthouse Scores (Before/After)
| Metric | Next.js 14 | Next.js 16 | Δ |
|---|---|---|---|
| Performance | 87 | 98 | +11 |
| First Contentful Paint | 1.2s | 0.7s | -42% |
| Time to Interactive | 2.8s | 1.1s | -61% |
| Total Bundle Size | 185KB | 92KB | -50% |
| SEO Score | 95 | 100 | +5 |
Real User Metrics (RUM)
Dati da Google Analytics dopo 30 giorni dal lancio:
- Average page load: 0.9s (era 2.1s)
- Bounce rate: 12% (era 28%)
- Pages per session: 3.2 (era 1.8)
- Session duration: 4:23 min (era 2:11 min)
Sfide e Lezioni Apprese
1. Hydration Mismatch Errors
Problema: Server Component rende HTML diverso da Client Component
// ❌ Causa hydration mismatch
export default function Time() {
return {new Date().toLocaleString()}
}
// ✅ Soluzione: use effect o suppressHydrationWarning
'use client'
export default function Time() {
const [time, setTime] = useState('')
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return {time || 'Loading...'}
}
2. Third-party Libraries non Compatibili
Problema: Alcune librerie non funzionano con Server Components
// ❌ Error: 'window' is not defined
import SomeLibrary from 'some-library'
// ✅ Soluzione: dynamic import con ssr: false
'use client'
import dynamic from 'next/dynamic'
const SomeLibrary = dynamic(() => import('some-library'), {
ssr: false
})
3. Caching Aggressivo
Problema: Next.js 16 cache aggressivo di fetch requests
// ❌ Cache indefinito
const data = await fetch('https://api.example.com/data')
// ✅ Disabilita cache esplicito
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// ✅ Revalidate ogni 60 secondi
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
Best Practices per Next.js 16
- Default to Server Components: Usa 'use client' solo quando necessario
- Colocate Server/Client: Componi Server e Client Components strategicamente
- Minimize Client JS: Sposta logica non interattiva sul server
- Use Suspense: Wrap async components in Suspense per loading states
- Optimize Images: Usa next/image per automatic optimization
- Configure Caching: Sii esplicito con fetch caching strategy
Conclusioni
La migrazione a Next.js 16 ha portato miglioramenti significativi a THEJORD:
- ✅ Performance: 3x più veloce in build, 7x in dev
- ✅ User Experience: Load time dimezzato, bundle size -50%
- ✅ SEO: Lighthouse 100/100, perfect metadata
- ✅ Developer Experience: Hot reload istantaneo, meno boilerplate
Consiglieresti Next.js 16? Assolutamente sì per progetti nuovi. Per migration, valuta complexity del codebase: App Router richiede refactoring significativo.