
Nextjs Performance
- 2.3k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
nextjs-performance is an agent skill that Expert Next.js performance optimization skill covering Core Web Vitals, image/font optimization, caching strategies, str.
About
Expert guidance for optimizing Next js applications with focus on Core Web Vitals modern patterns and best practices This skill provides comprehensive guidance for optimizing Next js applications It covers Core Web Vitals optimization LCP INP CLS modern React patterns Server Components caching strategies and bundle optimization techniques Designed for developers already familiar with React Next js who want to implement production grade optimizations Use this skill when working on Next js applications and need to Optimize Core Web Vitals LCP INP CLS for better performance and SEO Implement image optimization with next image for faster loading Configure font optimization with next font to eliminate layout shift Set up caching strategies using unstable_cache revalidateTag or ISR Convert Client Components to Server Components for reduced bundle size Implement Suspense streaming for progressive page loading Analyze and reduce bundle size with code splitting and dynamic imports Configure metadata and SEO for better search engine visibility Optimize API route handlers for better performance Apply Next js 16 and React 19 modern patterns Core
- description: Expert Next.js performance optimization skill covering Core Web Vitals, image/font optimization, caching st
- allowed-tools: Read, Write, Edit, Bash, Glob, Grep
- Expert guidance for optimizing Next.js applications with focus on Core Web Vitals, modern patterns, and best practices.
- Follow nextjs-performance SKILL.md steps and documented constraints.
- Follow nextjs-performance SKILL.md steps and documented constraints.
Nextjs Performance by the numbers
- 2,343 all-time installs (skills.sh)
- +106 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #350 of 16,659 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
nextjs-performance capabilities & compatibility
- Capabilities
- description: expert next.js performance optimiza · allowed tools: read, write, edit, bash, glob, gr · expert guidance for optimizing next.js applicati · follow nextjs performance skill.md steps and doc
- Use cases
- orchestration
What nextjs-performance says it does
description: Expert Next.js performance optimization skill covering Core Web Vitals, image/font optimization, caching strategies, streaming, bundle optimization, and Server Components best practices.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
Expert guidance for optimizing Next.js applications with focus on Core Web Vitals, modern patterns, and best practices.
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
When should an agent use nextjs-performance and what problem does it solve?
Expert Next.js performance optimization skill covering Core Web Vitals, image/font optimization, caching strategies, streaming, bundle optimization, and Server Components best practices. Use when opti
Who is it for?
Developers invoking nextjs-performance as documented in the skill source.
Skip if: Skip when requirements fall outside nextjs-performance documented scope.
When should I use this skill?
Expert Next.js performance optimization skill covering Core Web Vitals, image/font optimization, caching strategies, streaming, bundle optimization, and Server Components best practices. Use when opti
What you get
Outputs aligned with the nextjs-performance SKILL.md workflow and stated deliverables.
- optimized route.ts handlers
- cache export configuration
- streaming API patterns
By the numbers
- Documents revalidate = 3600 second caching example
Files
Next.js Performance Optimization
Expert guidance for optimizing Next.js applications with focus on Core Web Vitals, modern patterns, and best practices.
Overview
This skill provides comprehensive guidance for optimizing Next.js applications. It covers Core Web Vitals optimization (LCP, INP, CLS), modern React patterns, Server Components, caching strategies, and bundle optimization techniques. Designed for developers already familiar with React/Next.js who want to implement production-grade optimizations.
When to Use
Use this skill when working on Next.js applications and need to:
- Optimize Core Web Vitals (LCP, INP, CLS) for better performance and SEO
- Implement image optimization with
next/imagefor faster loading - Configure font optimization with
next/fontto eliminate layout shift - Set up caching strategies using
unstable_cache,revalidateTag, or ISR - Convert Client Components to Server Components for reduced bundle size
- Implement Suspense streaming for progressive page loading
- Analyze and reduce bundle size with code splitting and dynamic imports
- Configure metadata and SEO for better search engine visibility
- Optimize API route handlers for better performance
- Apply Next.js 16 and React 19 modern patterns
Coverage Areas
- Core Web Vitals optimization (LCP, INP, CLS)
- Image optimization with
next/image - Font optimization with
next/font - Caching strategies (
unstable_cache,revalidateTag, ISR) - Server Components patterns and Client-to-Server conversion
- Streaming and Suspense for progressive loading
- Bundle optimization and code splitting
- Metadata and SEO configuration
- Route handlers optimization
- Next.js 16 + React 19 patterns
Instructions
Before Starting
1. Analyze current performance with Lighthouse 2. Identify bottlenecks - check Core Web Vitals in Chrome DevTools or PageSpeed Insights 3. Determine optimization priority:
- LCP issues → Focus on images, fonts
- INP issues → Reduce JS, use Server Components
- CLS issues → Add dimensions, use next/font
How to Use This Skill
1. Load relevant reference files based on the area you're optimizing:
- Image issues →
references/image-optimization.md - Font/layout shift →
references/font-optimization.md - Caching →
references/caching-strategies.md - Component architecture →
references/server-components.md
2. Follow the quick patterns for common optimizations 3. Apply before/after conversions to improve existing code 4. Verify improvements with Lighthouse after changes
Core Principles
1. Prefer Server Components - Only use 'use client' when necessary (browser APIs, interactivity) 2. Load components as low as possible - Keep Client Components at leaf nodes 3. Use Suspense boundaries - Enable streaming and progressive loading 4. Cache appropriately - Use tags for granular revalidation 5. Measure before/after - Always verify improvements with real metrics
Examples
Example 1: Convert Client Component to Server Component
BEFORE (Client Component with useEffect):
'use client'
import { useEffect, useState } from 'react'
export default function ProductList() {
const [products, setProducts] = useState([])
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts)
}, [])
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}AFTER (Server Component with direct data access):
import { db } from '@/lib/db'
export default async function ProductList() {
const products = await db.product.findMany()
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}Example 2: Optimize Images for LCP
import Image from 'next/image'
export function Hero() {
return (
<div className="relative w-full h-[600px]">
<Image
src="/hero.jpg"
alt="Hero"
fill
priority // Disable lazy loading for LCP
sizes="100vw"
className="object-cover"
/>
</div>
)
}Example 3: Implement Caching Strategy
import { unstable_cache, revalidateTag } from 'next/cache'
// Cached data function
const getProducts = unstable_cache(
async () => db.product.findMany(),
['products'],
{ revalidate: 3600, tags: ['products'] }
)
// Revalidate on mutation
export async function createProduct(data: FormData) {
'use server'
await db.product.create({ data })
revalidateTag('products')
}Example 4: Setup Optimized Fonts
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body className={`${inter.className} antialiased`}>
{children}
</body>
</html>
)
}Example 5: Implement Suspense Streaming
import { Suspense } from 'react'
export default function Page() {
return (
<>
<header>Static content (immediate)</header>
<Suspense fallback={<ProductSkeleton />}>
<ProductList /> {/* Streamed when ready */}
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews /> {/* Independent streaming */}
</Suspense>
</>
)
}Reference Documentation
Load these references when working on specific areas:
| Topic | Reference File |
|---|---|
| Core Web Vitals | references/core-web-vitals.md |
| Image Optimization | references/image-optimization.md |
| Font Optimization | references/font-optimization.md |
| Caching Strategies | references/caching-strategies.md |
| Server Components | references/server-components.md |
| Streaming/Suspense | references/streaming-suspense.md |
| Bundle Optimization | references/bundle-optimization.md |
| Metadata/SEO | references/metadata-seo.md |
| API Routes | references/api-routes.md |
| Next.js 16 Patterns | references/nextjs-16-patterns.md |
Common Conversions
| From | To | Benefit |
|---|---|---|
useEffect + fetch | Direct async in Server Component | -70% JS, faster TTFB |
useState for data | Server Component with direct DB access | Simpler code, no hydration |
| Client-side fetch | unstable_cache or ISR | Faster repeated loads |
img tag | next/image | Optimized formats, lazy loading |
| CSS font import | next/font | Zero CLS, automatic optimization |
| Static import of heavy component | dynamic() | Reduced initial bundle |
Best Practices
Images
- Use
next/imagefor all images - Add
priorityto LCP images only - Provide
widthandheightorfillwith sizes - Use
placeholder="blur"for better UX - Configure remotePatterns in next.config.js
Fonts
- Use
next/fontinstead of CSS imports - Specify
subsetsto reduce size - Use
display: 'swap'for immediate text render - Create CSS variable with
variableoption - Configure Tailwind to use CSS variables
Caching
- Cache expensive queries with
unstable_cache - Use meaningful cache tags for granular control
- Implement on-demand revalidation for dynamic content
- Set TTL based on data change frequency
- Use revalidatePath for route-level invalidation
Components
- Convert Client Components to Server Components where possible
- Keep Client Components at the leaf level
- Use Suspense boundaries for progressive loading
- Implement proper loading states
- Use dynamic() for heavy components below the fold
Bundle
- Lazy load heavy components with
dynamic() - Use named exports for better tree shaking
- Analyze bundle regularly with
@next/bundle-analyzer - Prefer ESM packages over CommonJS
- Use modularizeImports for large libraries
Constraints and Warnings
Server Components Limitations
- Cannot use browser APIs (window, localStorage, document)
- Cannot use React hooks (useState, useEffect, useContext)
- Cannot use event handlers (onClick, onSubmit)
- Cannot use dynamic imports with ssr: false
Image Optimization Constraints
priorityshould only be used for above-the-fold images- External images require configuration in next.config.js
widthandheightare required unless usingfill- Animated GIFs are not optimized by default
Caching Considerations
- Cache tags must be manually invalidated
- Data cache is per-request in development
- Edge runtime has different caching behavior
- Be careful caching user-specific data
Bundle Size Warnings
- Dynamic imports can impact SEO if critical content
- Tree shaking requires proper ES module usage
- Some libraries cannot be tree shaken (avoid barrel exports)
- Client Components increase bundle size - use sparingly
Next.js 16 + React 19 Specifics
Async Params
// Next.js 15+ params is a Promise
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetchPost(slug)
return <article>{post.content}</article>
}use() Hook for Promises
'use client'
import { use, Suspense } from 'react'
function Comments({ promise }: { promise: Promise<Comment[]> }) {
const comments = use(promise) // Suspend until resolved
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>
}useOptimistic for UI Updates
'use client'
import { useOptimistic } from 'react'
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
async function addTodo(formData: FormData) {
const text = formData.get('text') as string
addOptimisticTodo({ id: crypto.randomUUID(), text, completed: false })
await createTodo(text)
}
return (
<form action={addTodo}>
<input name="text" />
{optimisticTodos.map(todo => <div key={todo.id}>{todo.text}</div>)}
</form>
)
}Bundle Analysis
# Install analyzer
npm install --save-dev @next/bundle-analyzer
# Run analysis
ANALYZE=true npm run build// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
modularizeImports: {
'lodash': { transform: 'lodash/{{member}}' },
},
})Performance Checklist
- [ ] All images use
next/imagewith proper dimensions - [ ] LCP images have
priorityattribute - [ ] Fonts use
next/fontwith subsets - [ ] Server Components used where possible
- [ ] Client Components at leaf level only
- [ ] Suspense boundaries for data fetching
- [ ] Caching configured for expensive operations
- [ ] Bundle analyzed for duplicates
- [ ] Heavy components lazy loaded
- [ ] Lighthouse score verified before/after
Common Mistakes
// ❌ DON'T: Fetch in useEffect
'use client'
useEffect(() => { fetch('/api/data').then(...) }, [])
// ✅ DO: Fetch directly in Server Component
const data = await fetch('/api/data')
// ❌ DON'T: Forget dimensions on images
<Image src="/photo.jpg" />
// ✅ DO: Always provide dimensions
<Image src="/photo.jpg" width={800} height={600} />
// ❌ DON'T: Use priority on all images
<Image src="/photo1.jpg" priority />
<Image src="/photo2.jpg" priority />
// ✅ DO: Priority only for LCP
<Image src="/hero.jpg" priority />
<Image src="/photo.jpg" loading="lazy" />
// ❌ DON'T: Cache everything with same TTL
{ revalidate: 3600 }
// ✅ DO: Match TTL to data change frequency
{ revalidate: 86400 } // Categories rarely change
{ revalidate: 60 } // Comments change oftenExternal Resources
Route Handlers Ottimizzati
Overview
Route handlers in app/ directory per API endpoints con supporto streaming e edge runtime.
---
Pattern Base
GET Handler
// app/api/users/route.ts
import { NextResponse } from 'next/server'
export const dynamic = 'force-static'
export const revalidate = 3600
export async function GET() {
const users = await db.user.findMany()
return NextResponse.json(users)
}POST Handler
// app/api/users/route.ts
export async function POST(request: Request) {
try {
const body = await request.json()
const user = await db.user.create({
data: body,
})
return NextResponse.json(user, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
)
}
}---
Edge Runtime
// app/api/edge/route.ts
export const runtime = 'edge'
export const preferredRegion = 'iad1' // US East
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')
// Edge runtime: minore cold start, distribuito globalmente
const result = await fetch(`https://api.example.com/search?q=${query}`)
return new Response(await result.text(), {
headers: { 'content-type': 'application/json' },
})
}---
Streaming Response
// app/api/stream/route.ts
export const runtime = 'edge'
export async function POST(request: Request) {
const { prompt } = await request.json()
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
stream: true,
}),
})
// Stream la response direttamente
return new Response(response.body, {
headers: { 'Content-Type': 'text/event-stream' },
})
}---
Caching Headers
// app/api/data/route.ts
export async function GET() {
const data = await fetchData()
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
})
}
// Con ETag
export async function GET(request: Request) {
const data = await fetchData()
const etag = generateETag(data)
// Check If-None-Match
if (request.headers.get('If-None-Match') === etag) {
return new Response(null, { status: 304 })
}
return NextResponse.json(data, {
headers: {
ETag: etag,
'Cache-Control': 'public, max-age=3600',
},
})
}---
Middleware
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// CORS headers
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// Rate limiting semplice
const ip = request.ip ?? 'anonymous'
const limit = await checkRateLimit(ip)
if (!limit.success) {
return new NextResponse('Rate limit exceeded', { status: 429 })
}
return response
}
export const config = {
matcher: '/api/:path*',
}---
Error Handling
// app/api/error-handler.ts
import { NextResponse } from 'next/server'
export class APIError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code: string = 'INTERNAL_ERROR'
) {
super(message)
}
}
export function handleError(error: unknown) {
if (error instanceof APIError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
)
}
console.error(error)
return NextResponse.json(
{ error: 'Internal server error', code: 'INTERNAL_ERROR' },
{ status: 500 }
)
}
// Uso
import { APIError, handleError } from './error-handler'
export async function GET() {
try {
const data = await fetchData()
if (!data) {
throw new APIError('Not found', 404, 'NOT_FOUND')
}
return NextResponse.json(data)
} catch (error) {
return handleError(error)
}
}---
Route Groups
// app/api/(public)/health/route.ts
export async function GET() {
return NextResponse.json({ status: 'ok', timestamp: Date.now() })
}
// app/api/(private)/admin/route.ts
export async function GET() {
// Protetto da middleware auth
return NextResponse.json({ data: 'sensitive' })
}Bundle Optimization
Overview
Ottimizzazioni per ridurre il JavaScript bundle e migliorare i tempi di caricamento.
---
Code Splitting
Dynamic Imports
// BEFORE - Import statico, sempre nel bundle
import HeavyChart from './HeavyChart'
export default function Dashboard() {
return <HeavyChart />
}
// AFTER - Lazy loaded
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Disabilita SSR se necessario
})
export default function Dashboard() {
return <HeavyChart />
}Condizionale Loading
'use client'
import dynamic from 'next/dynamic'
const MapComponent = dynamic(() => import('./Map'), {
ssr: false,
loading: () => <MapPlaceholder />,
})
export function LocationSection({ showMap }: { showMap: boolean }) {
// Componente caricato solo quando showMap è true
return showMap ? <MapComponent /> : null
}Import con Named Exports
const DynamicComponent = dynamic(
() => import('./components').then((mod) => mod.HeavyChart),
{
loading: () => <Loading />,
}
)---
Tree Shaking
Export Named vs Default
// ✅ SÌ: Named exports per tree shaking
export { Button, Input, Select }
// ❌ NON: Tutto in un oggetto
export default { Button, Input, Select }
// ❌ NON: Re-export wildcards
export * from 'lodash' // Importa tutto lodash
// ✅ SÌ: Import specifici
import { debounce } from 'lodash-es'Package.json Side Effects
{
"name": "my-lib",
"sideEffects": false,
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}---
Bundle Analysis
@next/bundle-analyzer
npm install --save-dev @next/bundle-analyzer// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// config
})ANALYZE=true npm run build---
Ottimizzazioni Librerie
Modular Imports (MUI)
// next.config.js
module.exports = {
modularizeImports: {
'@mui/material': {
transform: '@mui/material/{{member}}',
},
'@mui/icons-material': {
transform: '@mui/icons-material/{{member}}',
},
lodash: {
transform: 'lodash/{{member}}',
},
},
}ESM over CommonJS
// ❌ NON: CommonJS
const lodash = require('lodash')
// ✅ SÌ: ESM
import { debounce } from 'lodash-es'
// next.config.js per preferire ESM
module.exports = {
experimental: {
esmExternals: true,
},
}---
Ottimizzazione Dependencies
# Analizza bundle
npx webpack-bundle-analyzer .next/stats.json
# Trova duplicate dependencies
npx depcheck
# Bundle size check
npm run build 2>&1 | grep -E "(First Load JS|/api)"next.config.js Ottimizzazioni
/** @type {import('next').NextConfig} */
const nextConfig = {
// Ottimizzazione webpack
webpack: (config, { isServer }) => {
// Split chunks più aggressivo
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 2,
},
},
}
return config
},
// Ottimizzazioni build
swcMinify: true,
// Compressione
compress: true,
}
module.exports = nextConfig---
Best Practices
// ✅ SÌ: Lazy load componenti pesanti
const HeavyEditor = dynamic(() => import('./Editor'), { ssr: false })
// ✅ SÌ: Intersection Observer per below-fold
'use client'
import { useEffect, useRef, useState } from 'react'
export function LazyComponent({ component: Component }) {
const [shouldLoad, setShouldLoad] = useState(false)
const ref = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setShouldLoad(true)
observer.disconnect()
}
})
if (ref.current) observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return <div ref={ref}>{shouldLoad ? <Component /> : <Placeholder />}</div>
}
// ✅ SÌ: Prefetch route importante
import Link from 'next/link'
<Link href="/dashboard" prefetch={true}>Dashboard</Link>
// ❌ NON: Prefetch tutto
<Link href="/rarely-used" prefetch={true}>Rare</Link>Caching Strategies in Next.js
Overview
Next.js offre multiple strategie di caching:
- Request Memoization: Deduplica fetch nello stesso render
- Data Cache: Cache persistente tra request
- Full Route Cache: Cache delle pagine statiche
- Router Cache: Cache client-side delle route
---
Fetch Caching
Default Behavior (Next.js 15+)
// BEFORE (Next.js 14) - Cache di default
fetch('https://api.example.com/data') // cached
// AFTER (Next.js 15) - No cache di default
fetch('https://api.example.com/data') // no-store
fetch('https://api.example.com/data', { cache: 'force-cache' }) // cachedCache Time-based (ISR)
// Revalidazione automatica ogni 60 secondi
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: {
revalidate: 60, // secondi
tags: ['products'],
},
})
return res.json()
}On-demand Revalidation
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const { tag, path } = await request.json()
try {
if (tag) {
revalidateTag(tag)
return Response.json({ revalidated: true, tag })
}
if (path) {
revalidatePath(path)
return Response.json({ revalidated: true, path })
}
return Response.json({ error: 'Missing tag or path' }, { status: 400 })
} catch (error) {
return Response.json({ error: 'Revalidation failed' }, { status: 500 })
}
}
// Uso da webhook o admin
await fetch('/api/revalidate', {
method: 'POST',
body: JSON.stringify({ tag: 'products' }),
})---
unstable_cache
Cache di Funzioni
import { unstable_cache } from 'next/cache'
// BEFORE - Query ad ogni richiesta
async function getProducts() {
return db.product.findMany({ include: { category: true } })
}
// AFTER - Cache con revalidation
const getCachedProducts = unstable_cache(
async () => {
return db.product.findMany({ include: { category: true } })
},
['products'], // Cache key
{
revalidate: 3600, // 1 ora
tags: ['products', 'inventory'],
}
)
// Uso nel componente
export default async function ProductPage() {
const products = await getCachedProducts()
return <ProductList products={products} />
}Cache con Parametri
const getCachedProduct = unstable_cache(
async (id: string) => {
return db.product.findUnique({ where: { id } })
},
['product'], // Key base
{ tags: ['products'] }
)
// Cache key finale: ['product', '123']
const product = await getCachedProduct('123')---
Route Segment Config
Static vs Dynamic
// app/page.tsx
// Static (default se no dynamic data)
export const dynamic = 'auto'
// Forza statico
export const dynamic = 'force-static'
// Forza dinamico (no cache)
export const dynamic = 'force-dynamic'
// Error se usa dynamic data
export const dynamic = 'error'
// Revalidation
export const revalidate = 3600 // 1 ora
export const revalidate = false // Mai (default static)
export const revalidate = 0 // Ogni richiesta (dynamic)Runtime
// Edge runtime (più veloce, meno features)
export const runtime = 'edge'
// Node.js runtime (default, più compatibile)
export const runtime = 'nodejs'---
Server Actions Cache
// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function createProduct(formData: FormData) {
const data = Object.fromEntries(formData)
await db.product.create({ data })
// Revalidate specifiche route
revalidatePath('/products')
revalidatePath('/admin/products')
revalidateTag('products')
return { success: true }
}
export async function updateProduct(id: string, data: FormData) {
await db.product.update({ where: { id }, data: Object.fromEntries(data) })
// Revalidate specifico
revalidatePath(`/products/${id}`)
revalidateTag(`product-${id}`)
revalidateTag('products')
}---
Route Handlers Cache
// app/api/products/route.ts
// Static route con revalidation
export const dynamic = 'force-static'
export const revalidate = 60
export async function GET() {
const products = await db.product.findMany()
return Response.json(products)
}Handler Dinamici con Cache
// app/api/products/[id]/route.ts
export const dynamic = 'force-static'
export const revalidate = 3600
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const product = await db.product.findUnique({
where: { id },
})
if (!product) {
return Response.json({ error: 'Not found' }, { status: 404 })
}
return Response.json(product)
}
// Genera static params per build
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { id: true } })
return products.map((p) => ({ id: p.id }))
}---
Pattern Avanzati
Stale-While-Revalidate Pattern
import { unstable_cache } from 'next/cache'
const getData = unstable_cache(
async () => fetchExpensiveData(),
['expensive-data'],
{
revalidate: 3600, // 1 ora
}
)
// Con fetch diretto
async function getFreshData() {
const res = await fetch('https://api.example.com/data', {
next: {
revalidate: 3600,
},
})
return res.json()
}Cache Tagging Granulare
// Cache per differenti entità
const getUser = unstable_cache(
async (id: string) => db.user.findById(id),
['user'],
{ tags: (id) => [`user-${id}`, 'users'] }
)
const getUserOrders = unstable_cache(
async (userId: string) => db.order.findByUser(userId),
['user-orders'],
{ tags: (userId) => [`user-${userId}-orders`, 'orders'] }
)
// Invalidazione selettiva
revalidateTag('user-123') // Solo user
revalidateTag('user-123-orders') // Solo orders di user-123
revalidateTag('orders') // Tutti gli orders
revalidateTag('users') // Tutti gli usersCache con Headers Condizionali
// Route handler con ETag
export async function GET() {
const data = await getData()
const etag = generateETag(data)
const headers = new Headers()
headers.set('ETag', etag)
headers.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400')
return new Response(JSON.stringify(data), { headers })
}---
Best Practices
// ✅ SÌ: Cache con tags significativi
const getData = unstable_cache(fetchData, ['key'], {
revalidate: 3600,
tags: ['entity-type', 'entity-id'],
})
// ✅ SÌ: Revalidate selettivo
revalidateTag('user-123') // Non tutto 'users'
// ✅ SÌ: Differenti TTL per differenti dati
// Dati raramente modificati: lungo TTL
const getCategories = unstable_cache(fetchCategories, ['categories'], {
revalidate: 86400, // 24 ore
})
// Dati frequentemente modificati: breve TTL
const getComments = unstableCache(fetchComments, ['comments'], {
revalidate: 60, // 1 minuto
})
// ❌ NON: Cache di tutto con stesso TTL
// ❌ NON: Dimenticare di revalidate dopo mutation
// ❌ NON: Usare cache per dati utente-specifici senza key appropriataCore Web Vitals - Next.js Optimization
Overview
Core Web Vitals (CWV) sono le metriche di performance critiche per l'esperienza utente e il SEO.
| Metrica | Target | Ottimizzazione Principale |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | Ottimizzare l'elemento più grande visibile |
| INP (Interaction to Next Paint) | < 200ms | Minimizzare JS sul thread principale |
| CLS (Cumulative Layout Shift) | < 0.1 | Riservare spazio per elementi dinamici |
---
LCP Optimization
Elementi che contribuiscono a LCP
1. <img> elementi 2. <image> dentro SVG 3. Video poster 4. Elementi con background-image 5. Block-level text elements
Strategie Next.js
// BEFORE - LCP lento
<img src="/hero.jpg" width={1200} height={600} />
// AFTER - LCP ottimizzato
import Image from 'next/image'
// Priority carica l'immagine con fetchpriority="high"
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // ← Essenziale per LCP
quality={80}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>Preload Critical Resources
// app/layout.tsx
export const metadata = {
other: {
preconnect: ['https://fonts.googleapis.com'],
dnsPrefetch: ['https://api.example.com'],
},
}
// O con next/head in page router
import Head from 'next/head'
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://api.example.com" />
</Head>---
INP Optimization
Strategie
1. Spostare logica pesante su Web Workers 2. Utilizzare Server Components per ridurre JS client 3. Debouncing/Throttling degli event handlers
// BEFORE - INP alto
<button onClick={() => heavyComputation()}>Click</button>
// AFTER - INP ottimizzato
'use client'
import { useTransition } from 'react'
export function OptimizedButton() {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(() => {
heavyComputation()
})
}
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Processing...' : 'Click'}
</button>
)
}---
CLS Optimization
Pattern comuni che causano CLS
// BEFORE - CLS alto
// Immagine senza dimensioni
<img src="/photo.jpg" />
// Font che cambia durante il load
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter');
</style>
// AFTER - CLS zero
import Image from 'next/image'
import { Inter } from 'next/font/google'
// Font ottimizzato con display: swap gestito automaticamente
const inter = Inter({ subsets: ['latin'] })
// Immagine con dimensioni esplicite
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
// O con fill per layout responsive
fill
sizes="(max-width: 768px) 100vw, 800px"
/>Riservare spazio per contenuti dinamici
// BEFORE - Layout shift quando i dati arrivano
export default function Page() {
const [data, setData] = useState(null)
useEffect(() => {
fetchData().then(setData)
}, [])
return <div>{data ? <Content data={data} /> : null}</div>
}
// AFTER - Spazio riservato
export default function Page() {
const [data, setData] = useState(null)
useEffect(() => {
fetchData().then(setData)
}, [])
return (
<div className="min-h-[400px]">
{data ? <Content data={data} /> : <Skeleton />}
</div>
)
}---
Monitoring CWV in Next.js
Vercel Analytics
npm i @vercel/analytics// app/layout.tsx
import { Analytics } from '@vercel/analytics/next'
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
<Analytics />
</html>
)
}Speed Insights
npm i @vercel/speed-insights// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
<SpeedInsights />
</html>
)
}Web Vitals API (Custom)
// app/_components/web-vitals.tsx
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
// Invia a analytics
console.log(metric)
// Esempio: invio a Google Analytics
if (window.gtag) {
window.gtag('event', metric.name, {
value: Math.round(metric.value),
event_category: 'Web Vitals',
event_label: metric.id,
non_interaction: true,
})
}
})
return null
}---
Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli@0.13.x
lhci autorun// lighthouserc.json
{
"ci": {
"collect": {
"startServerCommand": "npm start",
"url": ["http://localhost:3000"]
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"categories:best-practices": ["error", { "minScore": 0.9 }],
"categories:seo": ["error", { "minScore": 0.9 }]
}
}
}
}Font Optimization - next/font
Overview
next/font ottimizza automaticamente i font:
- Elimina layout shift (CLS)
- Automatic subsetting
- Preload dei font critici
- Zero runtime JavaScript
- Supporto Google Fonts e font locali
---
Configurazione Base
Google Font
// BEFORE - Layout shift, FOUT
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet" />
// AFTER - Zero layout shift
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body className={`${inter.className} antialiased`}>
{children}
</body>
</html>
)
}Local Font
import localFont from 'next/font/local'
const myFont = localFont({
src: [
{
path: './fonts/Custom-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/Custom-Bold.woff2',
weight: '700',
style: 'normal',
},
{
path: './fonts/Custom-Italic.woff2',
weight: '400',
style: 'italic',
},
],
variable: '--font-custom',
display: 'swap',
})---
Pattern Comuni
Multiple Fonts
// fonts.ts
import { Inter, Playfair_Display } from 'next/font/google'
export const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
export const playfair = Playfair_Display({
subsets: ['latin'],
variable: '--font-playfair',
display: 'swap',
})
// app/layout.tsx
import { inter, playfair } from './fonts'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${playfair.variable}`}>
<body className={`${inter.className} antialiased`}>
{children}
</body>
</html>
)
}
// tailwind.config.ts
import type { Config } from 'tailwindcss'
const config: Config = {
content: ['./app/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
serif: ['var(--font-playfair)', 'Georgia', 'serif'],
},
},
},
plugins: [],
}
export default configFont con Tailwind CSS v4
/* app/globals.css con Tailwind v4 */
@import "tailwindcss";
@theme {
--font-sans: var(--font-inter), ui-sans-serif, system-ui;
--font-serif: var(--font-playfair), ui-serif, Georgia;
}Variable Fonts (Consigliato)
// BEFORE - Multipli file per ogni weight
// AFTER - Un solo file per tutti i weight
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
// Inter è una variable font - un solo file
})
// Uso con qualsiasi weight
<p className="font-sans font-light">Light text</p>
<p className="font-sans font-normal">Normal text</p>
<p className="font-sans font-bold">Bold text</p>Font Ottimizzati per Performance
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
// Aggiungi preconnect per velocizzare il download
adjustFontFallback: true, // Font fallback ottimizzato
})
// Preconnect in layout
export const metadata = {
other: {
preconnect: ['https://fonts.googleapis.com', 'https://fonts.gstatic.com'],
},
}---
Ottimizzazioni Avanzate
Preload Font Critici
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<head>
{/* Preload del font critico */}
<link
rel="preload"
href="/fonts/custom-font.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</head>
<body className={inter.className}>{children}</body>
</html>
)
}Font con CSS Fallback Ottimizzato
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
// Next.js genera automaticamente un fallback ottimizzato
// basato sulle metriche del font scelto
adjustFontFallback: true,
})
// CSS personalizzato per ridurre FOUT
// globals.css
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
ascent-override: 90.49%;
descent-override: 22.52%;
line-gap-override: 0%;
size-adjust: 107.06%;
}Font Condizionali per Lingue
// app/[lang]/layout.tsx
import { Inter, Noto_Sans_JP } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const notoJP = Noto_Sans_JP({ subsets: ['latin'], variable: '--font-noto-jp' })
export default function RootLayout({
children,
params: { lang },
}: {
children: React.ReactNode
params: { lang: string }
}) {
const fontClass = lang === 'ja' ? notoJP.variable : inter.variable
const bodyClass = lang === 'ja' ? notoJP.className : inter.className
return (
<html lang={lang} className={fontClass}>
<body className={bodyClass}>{children}</body>
</html>
)
}---
Errori Comuni
// ❌ NON: Importare il CSS dei font manualmente
import 'google-fonts/inter.css'
// ✅ SÌ: Usare sempre next/font
import { Inter } from 'next/font/google'
// ❌ NON: Dimenticare subsets (aumenta dimensione)
const inter = Inter({}) // Carica tutti i caratteri
// ✅ SÌ: Specificare subsets
const inter = Inter({ subsets: ['latin'] })
// ❌ NON: Usare display: block (no text visibile durante il load)
const inter = Inter({ display: 'block' })
// ✅ SÌ: Usare swap per immediate text render
const inter = Inter({ display: 'swap' })
// ❌ NON: Importare font in ogni componente
// components/Button.tsx
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] }) // ❌ Doppio caricamento
// ✅ SÌ: Importare una sola volta in layout
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })---
Performance Checklist
- [ ] Usare
subsetsper ridurre dimensione font - [ ] Preferire variable fonts quando disponibili
- [ ] Usare
display: 'swap'per evitare invisible text - [ ] Aggiungere
variableper CSS custom properties - [ ] Configurare Tailwind per usare le variabili CSS
- [ ] Preconnect a fonts.googleapis.com e fonts.gstatic.com
- [ ] Usare
adjustFontFallback: trueper ridurre CLS
Image Optimization - next/image
Overview
Next.js fornisce un componente Image ottimizzato che:
- Ottimizza automaticamente le immagini
- Serve formati moderni (WebP, AVIF)
- Responsive images automatiche
- Lazy loading nativo
- Previene layout shift
---
Configurazione Base
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// Domini esterni consentiti
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
port: '',
pathname: '/images/**',
},
],
// Formati supportati (ordine di preferenza)
formats: ['image/avif', 'image/webp'],
// Qualità di default (1-100)
quality: 75,
// Dimensioni per responsive images
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
module.exports = nextConfig---
Pattern Comuni
1. Immagine Hero (LCP)
// BEFORE
<img src="/hero.jpg" className="w-full h-auto" />
// AFTER
import Image from 'next/image'
export function Hero() {
return (
<div className="relative w-full h-[600px]">
<Image
src="/hero.jpg"
alt="Hero image"
fill
priority // ← Disabilita lazy loading per LCP
quality={85}
className="object-cover"
sizes="100vw"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
/>
</div>
)
}2. Immagini Responsive
// BEFORE - Stessa immagine per tutti i device
<img src="/photo-large.jpg" />
// AFTER - Immagini adattive
import Image from 'next/image'
export function ResponsiveImage() {
return (
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
// Genera srcset automaticamente:
// 640w, 750w, 828w, 1080w, 1200w...
/>
)
}3. Grid di Immagini
// AFTER - Grid ottimizzata
import Image from 'next/image'
export function ImageGrid({ images }: { images: { src: string; alt: string }[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{images.map((img, i) => (
<div key={i} className="relative aspect-square">
<Image
src={img.src}
alt={img.alt}
fill
sizes="(max-width: 768px) 100vw, 33vw"
className="object-cover rounded-lg"
loading={i < 3 ? 'eager' : 'lazy'} // Prime 3 eager, resto lazy
/>
</div>
))}
</div>
)
}4. Immagini da CMS/CDN Esterno
// AFTER - Configurazione con loader personalizzato
import Image from 'next/image'
// Se il CMS ha le sue ottimizzazioni
const contentfulLoader = ({ src, width, quality }: {
src: string
width: number
quality?: number
}) => {
return `${src}?w=${width}&q=${quality || 75}&fm=webp`
}
export function CMSImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
loader={contentfulLoader}
src={src}
alt={alt}
width={800}
height={600}
/>
)
}---
Placeholder e Loading States
Blur Placeholder
// Generare blurDataURL (lato build o API)
import { getPlaiceholder } from 'plaiceholder'
async function getBlurData(src: string) {
const buffer = await fetch(src).then(async (res) =>
Buffer.from(await res.arrayBuffer())
)
const { base64 } = await getPlaiceholder(buffer)
return base64
}
// Uso nel componente
export async function ImageWithBlur({ src, alt }: { src: string; alt: string }) {
const blurDataURL = await getBlurData(src)
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
placeholder="blur"
blurDataURL={blurDataURL}
/>
)
}Color Placeholder
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect width='1' height='1' fill='%23e2e8f0'/%3E%3C/svg%3E"
/>---
Art Direction (Picture Element)
// BEFORE - Stessa immagine per tutti
<img src="/landscape.jpg" />
// AFTER - Art direction con picture
export function ArtDirectedImage() {
return (
<picture>
{/* Mobile: portrait crop */}
<source
media="(max-width: 768px)"
srcSet="/photo-mobile.jpg"
width={400}
height={600}
/>
{/* Tablet: square crop */}
<source
media="(max-width: 1024px)"
srcSet="/photo-tablet.jpg"
width={600}
height={600}
/>
{/* Desktop: full image */}
<img
src="/photo-desktop.jpg"
alt="Responsive photo"
width={1200}
height={800}
/>
</picture>
)
}---
Ottimizzazione Avanzata
Preload Critical Images
// app/page.tsx
import Image from 'next/image'
export default function Page() {
return (
<>
{/* Preload per immagine LCP */}
<link
rel="preload"
href="/hero.jpg"
as="image"
type="image/jpeg"
/>
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>
</>
)
}SVG come Componenti (non come img)
// BEFORE - SVG come img (non ottimale)
<Image src="/icon.svg" width={24} height={24} />
// AFTER - SVG inline per animazioni e styling
import Icon from './icon.svg'
export function Button() {
return (
<button>
<Icon className="w-6 h-6 text-blue-500" />
</button>
)
}
// next.config.js per supportare SVG come componenti
const nextConfig = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
})
return config
},
}Client-side Image Loading (con fallback)
'use client'
import Image from 'next/image'
import { useState } from 'react'
export function SafeImage({
src,
alt,
fallback = '/placeholder.jpg',
...props
}: {
src: string
alt: string
fallback?: string
} & React.ComponentProps<typeof Image>) {
const [imgSrc, setImgSrc] = useState(src)
return (
<Image
{...props}
src={imgSrc}
alt={alt}
onError={() => setImgSrc(fallback)}
/>
)
}---
Errori Comuni da Evitare
// ❌ NON: Usare width/height stringhe
<Image src="/photo.jpg" width="100%" height="auto" />
// ✅ SÌ: Usare numeri (px) o fill con parent sized
<Image src="/photo.jpg" width={800} height={600} />
// oppure
<div className="relative w-full h-64">
<Image src="/photo.jpg" fill />
</div>
// ❌ NON: Dimenticare alt text
<Image src="/photo.jpg" width={800} height={600} />
// ✅ SÌ: Sempre fornire alt significativo
<Image src="/photo.jpg" alt="Gatto che dorme sul divano" width={800} height={600} />
// ❌ NON: Usare priority su tutte le immagini
<Image src="/photo1.jpg" priority />
<Image src="/photo2.jpg" priority />
<Image src="/photo3.jpg" priority />
// ✅ SÌ: Priority solo per LCP image
<Image src="/hero.jpg" priority />
<Image src="/photo1.jpg" loading="lazy" />
<Image src="/photo2.jpg" loading="lazy" />Metadata e SEO
Overview
Next.js fornisce l'API Metadata per gestire i meta tag HTML in modo dichiarativo e type-safe.
---
Pattern Base
Static Metadata
// app/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Page',
description: 'Page description for SEO',
}
export default function Page() {
return <div>Content</div>
}Dynamic Metadata
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await fetchPost(slug)
return {
title: post.title,
description: post.excerpt,
}
}---
OpenGraph e Social
export const metadata: Metadata = {
title: {
default: 'My Site',
template: '%s | My Site',
},
description: 'Site description',
openGraph: {
title: 'My Page',
description: 'Page description',
url: 'https://mysite.com',
siteName: 'My Site',
images: [
{
url: 'https://mysite.com/og-image.jpg',
width: 1200,
height: 630,
alt: 'My Site',
},
],
locale: 'it_IT',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'My Page',
description: 'Page description',
images: ['https://mysite.com/twitter-image.jpg'],
},
}---
Robots e Sitemap
robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/', '/private/'],
},
],
sitemap: 'https://mysite.com/sitemap.xml',
host: 'https://mysite.com',
}
}sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetchPosts()
const postEntries = posts.map((post) => ({
url: `https://mysite.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}))
return [
{
url: 'https://mysite.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...postEntries,
]
}---
Structured Data (JSON-LD)
// app/page.tsx
export default function Page() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'My Company',
url: 'https://mysite.com',
logo: 'https://mysite.com/logo.png',
sameAs: [
'https://twitter.com/mycompany',
'https://linkedin.com/company/mycompany',
],
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div>Content</div>
</>
)
}Article Structured Data
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await fetchPost(slug)
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author.name }],
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
images: [post.coverImage],
},
}
}---
Metadata Base
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://mysite.com'),
title: {
default: 'My Site',
template: '%s | My Site',
},
description: 'Default description',
keywords: ['nextjs', 'react', 'web development'],
authors: [{ name: 'Author Name' }],
creator: 'Author Name',
publisher: 'My Company',
alternates: {
canonical: '/',
languages: {
'en-US': '/en',
'it-IT': '/it',
},
},
verification: {
google: 'google-site-verification-code',
},
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
apple: '/apple-touch-icon.png',
},
manifest: '/site.webmanifest',
}Next.js 16 + React 19 Patterns
Overview
Nuove feature e pattern specifici per Next.js 16 con React 19.
---
Async Context
Async Server Components
// app/page.tsx
export default async function Page() {
// Direttamente async nel componente
const data = await fetch('https://api.example.com/data')
return <DataView data={data} />
}Async Layout
// app/layout.tsx
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const settings = await fetchSettings()
return (
<html lang={settings.locale}>
<body className={settings.theme}>{children}</body>
</html>
)
}---
Server Actions
Form Actions
// app/actions.ts
'use server'
export async function submitForm(formData: FormData) {
'use server'
const name = formData.get('name')
const email = formData.get('email')
await db.user.create({ data: { name, email } })
redirect('/success')
}
// app/page.tsx
import { submitForm } from './actions'
export default function Page() {
return (
<form action={submitForm}>
<input name="name" />
<input name="email" type="email" />
<button type="submit">Submit</button>
</form>
)
}useActionState
'use client'
import { useActionState } from 'react'
import { submitForm } from './actions'
export function Form() {
const [state, action, pending] = useActionState(submitForm, null)
return (
<form action={action}>
<input name="email" />
<button disabled={pending}>{pending ? 'Submitting...' : 'Submit'}</button>
{state?.error && <p>{state.error}</p>}
</form>
)
}useFormStatus
'use client'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return <button disabled={pending}>{pending ? 'Loading...' : 'Submit'}</button>
}
export function Form() {
return (
<form action={submitAction}>
<input name="name" />
<SubmitButton />
</form>
)
}---
React 19 Hooks
use()
'use client'
import { use, Suspense } from 'react'
// Promise in variabile
const messagePromise = fetchMessage()
function Message() {
const message = use(messagePromise)
return <p>{message}</p>
}
// O da props
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
)
}useOptimistic
'use client'
import { useOptimistic } from 'react'
type Todo = { id: string; text: string; completed: boolean }
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
async function addTodo(formData: FormData) {
const text = formData.get('text') as string
// Aggiungi subito UI
addOptimisticTodo({
id: crypto.randomUUID(),
text,
completed: false,
})
// Poi fai la richiesta reale
await createTodo(text)
}
return (
<form action={addTodo}>
<input name="text" />
<button>Add</button>
{optimisticTodos.map((todo) => (
<div key={todo.id}>{todo.text}</div>
))}
</form>
)
}---
Parallel Data Fetching
// app/page.tsx
import { Suspense } from 'react'
export default function Page() {
// Avvia tutte le fetch in parallelo
const userPromise = fetchUser()
const postsPromise = fetchPosts()
const statsPromise = fetchStats()
return (
<>
<Suspense fallback={<UserSkeleton />}>
<UserProfile promise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostList promise={postsPromise} />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Stats promise={statsPromise} />
</Suspense>
</>
)
}
// Componenti ricevono la promise
async function UserProfile({ promise }: { promise: Promise<User> }) {
const user = await promise
return <div>{user.name}</div>
}---
Incremental Static Regeneration (ISR)
// app/blog/[slug]/page.tsx
// Rivalida ogni ora
export const revalidate = 3600
export async function generateStaticParams() {
const posts = await fetchPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetchPost(slug)
return <article>{post.content}</article>
}---
Error Handling
Error Boundaries
// app/error.tsx
'use client'
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
)
}Not Found
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find requested resource</p>
</div>
)
}
// Uso nel componente
import { notFound } from 'next/navigation'
export default async function Page({ params }: { params: { id: string } }) {
const data = await fetchData(params.id)
if (!data) {
notFound()
}
return <div>{data.name}</div>
}Server Components - Best Practices
Overview
Server Components eseguono sul server:
- Zero JavaScript bundle size
- Accesso diretto a database/API
- Riduzione del tempo di hydration
- Accesso a risorse server-side
---
Pattern Base
Server Component Puro
// BEFORE (Client Component)
'use client'
import { useEffect, useState } from 'react'
export default function ProductList() {
const [products, setProducts] = useState([])
useEffect(() => {
fetch('/api/products')
.then(r => r.json())
.then(setProducts)
}, [])
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
// AFTER (Server Component)
import { db } from '@/lib/db'
export default async function ProductList() {
const products = await db.product.findMany()
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}Client Component Ibrido
// ProductCard.tsx - Server Component
import { AddToCartButton } from './AddToCartButton'
import { db } from '@/lib/db'
export async function ProductCard({ id }: { id: string }) {
const product = await db.product.findById(id)
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
{/* Solo il bottone è client component */}
<AddToCartButton productId={id} />
</div>
)
}
// AddToCartButton.tsx - Client Component
'use client'
import { useState } from 'react'
export function AddToCartButton({ productId }: { productId: string }) {
const [adding, setAdding] = useState(false)
const addToCart = async () => {
setAdding(true)
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId }),
})
setAdding(false)
}
return (
<button onClick={addToCart} disabled={adding}>
{adding ? 'Adding...' : 'Add to Cart'}
</button>
)
}---
React 19 + Next.js 16 Patterns
Async Params (Next.js 15+)
// BEFORE (Next.js 14)
export default async function Page({ params }: { params: { slug: string } }) {
const post = await fetchPost(params.slug)
// ...
}
// AFTER (Next.js 15+) - params è una Promise
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetchPost(slug)
// ...
}use() Hook (React 19)
'use client'
import { use } from 'react'
// Promise creata fuori dal componente
const messagePromise = fetchMessage()
export function Message() {
// Suspende finché la promise non resolve
const message = use(messagePromise)
return <p>{message}</p>
}
// Con props
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return (
<ul>
{comments.map(c => <li key={c.id}>{c.text}</li>)}
</ul>
)
}Server Actions Migliorati
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
// Action con validazione
export async function updateProfile(prevState: any, formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
// Validazione
if (!name || name.length < 2) {
return { error: 'Name must be at least 2 characters' }
}
try {
await db.user.update({
where: { id: session.userId },
data: { name, email },
})
revalidatePath('/profile')
return { success: true }
} catch (error) {
return { error: 'Update failed' }
}
}
// Componente con useActionState (React 19)
'use client'
import { useActionState } from 'react'
import { updateProfile } from './actions'
export function ProfileForm() {
const [state, action, pending] = useActionState(updateProfile, null)
return (
<form action={action}>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">Saved!</p>}
</form>
)
}---
Conversione Client → Server Component
Checklist di Conversione
1. Rimuovere 'use client' 2. Spostare data fetching nel componente 3. Rimuovere useState/useEffect per data loading 4. Spostare interattività in componenti figli
// BEFORE - Client Component
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
export default function ProductPage({ params }: { params: { id: string } }) {
const [product, setProduct] = useState(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
useEffect(() => {
fetch(`/api/products/${params.id}`)
.then(r => r.json())
.then(data => {
setProduct(data)
setLoading(false)
})
}, [params.id])
if (loading) return <div>Loading...</div>
return (
<div>
<h1>{product.name}</h1>
<button onClick={() => router.push('/products')}>Back</button>
</div>
)
}
// AFTER - Server Component + Client Component
// ProductPage.tsx (Server Component)
import { db } from '@/lib/db'
import { BackButton } from './BackButton'
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const product = await db.product.findById(id)
if (!product) {
notFound()
}
return (
<div>
<h1>{product.name}</h1>
<BackButton /> {/* Client component per interattività */}
</div>
)
}
// BackButton.tsx (Client Component)
'use client'
import { useRouter } from 'next/navigation'
export function BackButton() {
const router = useRouter()
return <button onClick={() => router.back()}>Back</button>
}---
Accesso a Risorse Server-side
Database Access
// lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const db = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
// Uso nel componente
import { db } from '@/lib/db'
export default async function Page() {
// Query diretta - nessuna API route necessaria
const users = await db.user.findMany()
// ...
}File System
import { readFile } from 'fs/promises'
import path from 'path'
export default async function Page() {
const filePath = path.join(process.cwd(), 'content', 'about.md')
const content = await readFile(filePath, 'utf-8')
return <Markdown content={content} />
}Environment Variables
// Server Components possono accedere a tutte le env vars
export default async function Page() {
// ✅ Accesso diretto alle variabili server
const apiKey = process.env.API_SECRET_KEY
const dbUrl = process.env.DATABASE_URL
const data = await fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${apiKey}` },
})
// ...
}---
Errori Comuni
// ❌ NON: Usare browser APIs in Server Component
export default function Page() {
const width = window.innerWidth // ❌ window non esiste sul server
localStorage.getItem('key') // ❌ localStorage non esiste sul server
}
// ✅ SÌ: Spostare in Client Component
'use client'
export function WindowSize() {
const [width, setWidth] = useState(window.innerWidth)
// ...
}
// ❌ NON: Usare hooks in Server Component
export default function Page() {
const [count, setCount] = useState(0) // ❌ Hook non funzionano
useEffect(() => {...}, []) // ❌
}
// ❌ NON: Dimenticare di gestire errori
export default async function Page() {
const data = await fetchData() // Può lanciare
// ...
}
// ✅ SÌ: Gestire errori
export default async function Page() {
try {
const data = await fetchData()
return <DataView data={data} />
} catch (error) {
return <ErrorMessage />
}
}
// ❌ NON: Troppi Client Components annidati
// ServerComponent
// → ClientComponent1
// → ClientComponent2
// → ClientComponent3
// ✅ SÌ: Mantenere Client Components il più in basso possibile
// ServerComponent
// → ServerComponent
// → ServerComponent
// → ClientComponent (solo dove necessario)Streaming e Suspense in Next.js
Overview
Streaming permette di inviare parti della UI man mano che sono pronte, migliorando il Time to First Byte (TTFB) e permettendo all'utente di vedere contenuto più velocemente.
---
Pattern Base
Loading.tsx
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="loading-skeleton">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse" />
<div className="mt-4 space-y-2">
{<>Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-4 bg-gray-200 rounded animate-pulse" />
))}</>
</div>
</div>
)
}Suspense Boundaries
// app/page.tsx
import { Suspense } from 'react'
import { ProductListSkeleton } from './components/ProductListSkeleton'
import { ProductList } from './components/ProductList'
import { ReviewsSkeleton } from './components/ReviewsSkeleton'
import { Reviews } from './components/Reviews'
export default function Page() {
return (
<div>
{/* Questo è streamato immediatamente */}
<header>
<h1>Our Products</h1>
</header>
{/* Suspense boundary per ProductList */}
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
{/* Reviews può caricare indipendentemente */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</div>
)
}---
Streaming Pattern
Waterfall Ottimizzato
// BEFORE - Waterfall sequenziale
export default async function Page() {
const user = await fetchUser() // 100ms
const orders = await fetchOrders() // 150ms (dipende da user)
const recommendations = await fetchRecommendations() // 200ms
return (
<div>
<UserProfile user={user} />
<OrderList orders={orders} />
<Recommendations items={recommendations} />
</div>
)
}
// AFTER - Parallel fetching con Suspense
export default function Page() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<OrderList />
</Suspense>
<Suspense fallback={<RecsSkeleton />}>
<Recommendations />
</Suspense>
</div>
)
}
// Ogni componente fetcha i propri dati
async function UserProfile() {
const user = await fetchUser()
return <div>{user.name}</div>
}
async function OrderList() {
const orders = await fetchOrders()
return <ul>{orders.map(o => <li key={o.id}>{o.total}</li>)}</ul>
}Nested Suspense
// app/dashboard/page.tsx
export default function DashboardPage() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
)
}
// app/dashboard/Dashboard.tsx
async function Dashboard() {
return (
<div>
<Sidebar />
<main>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</main>
</div>
)
}---
use() Hook (React 19)
'use client'
import { use, Suspense } from 'react'
// Promise creata fuori dal componente
const messagePromise = fetchMessage()
function Message() {
// Suspende finché la promise non resolve
const message = use(messagePromise)
return <p>{message}</p>
}
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<Message />
</Suspense>
)
}Con Props
'use client'
import { use } from 'react'
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return (
<ul>
{comments.map(c => <li key={c.id}>{c.text}</li>)}
</ul>
)
}
// Server Component che passa la promise
export default function Page() {
const commentsPromise = fetchComments() // Non await!
return (
<Suspense fallback={<Loading />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}---
Error Boundaries
// app/error.tsx
'use client'
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}---
Best Practices
// ✅ SÌ: Place Suspense boundaries strategically
<Suspense fallback={<SpecificSkeleton />}>
<ExpensiveComponent />
</Suspense>
// ❌ NON: Unico Suspense in alto livello
<Suspense fallback={<GenericLoading />}>
<EntirePage />
</Suspense>
// ✅ SÌ: Skeletons specifici per ogni sezione
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid />
</Suspense>
<Suspense fallback={<ReviewListSkeleton />}>
<ReviewList />
</Suspense>
// ✅ SÌ: Fetch nel componente che usa i dati
async function ProductList() {
const products = await fetchProducts() // Fetch locale
return <div>{...}</div>
}
// ❌ NON: Passare dati attraverso props
async function Page() {
const products = await fetchProducts() // ❌ Blocca tutta la pagina
return <ProductList products={products} />
}Related skills
Forks & variants (1)
Nextjs Performance has 1 known copy in the catalog totaling 22 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 22 installs
How it compares
Use nextjs-performance when you need App Router handler templates with explicit cache exports instead of default fully dynamic API routes.
FAQ
What is nextjs-performance?
Expert Next.js performance optimization skill covering Core Web Vitals, image/font optimization, caching strategies, streaming, bundle optimization, and Server Components best prac
When should I use nextjs-performance?
Expert Next.js performance optimization skill covering Core Web Vitals, image/font optimization, caching strategies, streaming, bundle optimization, and Server Components best prac
Is nextjs-performance safe to install?
Review the Security Audits panel on this page before production use.