Visão Geral do Next.js 15
O Next.js 15 é um lançamento principal que traz suporte oficial ao React 19 e estabilização do Turbopack. Melhorias significativas foram feitas tanto na experiência de desenvolvimento quanto no desempenho.
flowchart TB
subgraph Next15["Destaques do Next.js 15"]
subgraph Turbo["Turbopack (Estável para Dev)"]
T1["Inicialização do servidor dev: 76% mais rápido"]
T2["Fast Refresh: 96% mais rápido"]
T3["Compilação inicial: 45% mais rápido"]
end
subgraph React19["Suporte ao React 19"]
R1["Server Actions (Estável)"]
R2["use() Hook"]
R3["React Compiler (Experimental)"]
end
subgraph PPR["Partial Prerendering (Beta)"]
P1["Shell estático + Streaming de conteúdo dinâmico"]
end
subgraph Cache["Novos Padrões de Cache"]
C1["fetch: no-store (padrão)"]
C2["Route Handlers: no-store (padrão)"]
end
end
Estabilização do Turbopack
No Next.js 15, o Turbopack para o servidor de desenvolvimento tornou-se estável.
# Iniciar servidor de desenvolvimento com Turbopack
next dev --turbo
# Build ainda usa Webpack (Turbopack em desenvolvimento)
next build
Comparação de Desempenho
Comparação de desempenho em projeto de grande escala (5000 módulos):
| Item | Webpack | Turbopack | Melhoria |
|---|---|---|---|
| Inicialização do servidor dev | 8,2s | 2,0s | 76% mais rápido |
| Fast Refresh (alteração de 1 arquivo) | 800ms | 30ms | 96% mais rápido |
| Compilação inicial de rota | 600ms | 330ms | 45% mais rápido |
| Uso de memória | 1,6GB | 800MB | 50% de redução |
Suporte ao React 19
Server Actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validação
if (!title || title.length < 3) {
return { error: 'O título deve ter pelo menos 3 caracteres' };
}
// Operação no banco de dados
await db.posts.create({ title, content });
// Revalidação do cache
revalidatePath('/posts');
// Redirecionamento
redirect('/posts');
}
// Action com atualização otimista
export async function likePost(postId: string) {
await db.posts.update({
where: { id: postId },
data: { likes: { increment: 1 } },
});
revalidatePath(`/posts/${postId}`);
}
// app/posts/new/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Título" required />
<textarea name="content" placeholder="Conteúdo" required />
<button type="submit">Criar Post</button>
</form>
);
}
useActionState
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';
export function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
<form action={formAction}>
<input name="title" placeholder="Título" disabled={isPending} />
<textarea name="content" placeholder="Conteúdo" disabled={isPending} />
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Criando...' : 'Criar Post'}
</button>
</form>
);
}
useOptimistic
'use client';
import { useOptimistic } from 'react';
import { likePost } from '../actions';
export function LikeButton({ postId, initialLikes }: Props) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, _) => state + 1
);
async function handleLike() {
addOptimisticLike(null);
await likePost(postId);
}
return (
<form action={handleLike}>
<button type="submit">
❤️ {optimisticLikes}
</button>
</form>
);
}
Partial Prerendering (PPR)
Uma nova estratégia de renderização que combina shell estático com conteúdo dinâmico.
flowchart TB
subgraph BuildTime["Tempo de Build"]
subgraph StaticShell["Shell Estático"]
Header["Header (estático)"]
subgraph Layout["Layout"]
Content["Conteúdo (estático)"]
Suspense["Suspense Fallback<br/>(Skeleton)"]
end
end
end
subgraph RequestTime["Tempo de Requisição"]
Step1["1. Retorna shell estático imediatamente (TTFB: poucos ms)"]
Step2["2. Streaming da parte dinâmica"]
Step3["3. Boundaries do Suspense hidratam sequencialmente"]
Step1 --> Step2 --> Step3
end
BuildTime --> RequestTime
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
// app/products/[id]/page.tsx
import { Suspense } from 'react';
// Parte gerada estaticamente
export default function ProductPage({ params }: Props) {
return (
<div>
{/* Conteúdo estático */}
<ProductDetails id={params.id} />
{/* Conteúdo dinâmico (streaming) */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations userId={getUserId()} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<RecentReviews productId={params.id} />
</Suspense>
</div>
);
}
// Componente que usa dados dinâmicos
async function PersonalizedRecommendations({ userId }: { userId: string }) {
// Esta parte é executada no tempo de requisição
const recommendations = await getRecommendations(userId);
return <RecommendationList items={recommendations} />;
}
Nova Semântica de Cache
No Next.js 15, o comportamento padrão do cache foi alterado.
// Next.js 14 e anteriores: Cache por padrão
// Next.js 15: Sem cache por padrão
// Mudança no padrão da API fetch
async function getData() {
// Next.js 14: cache: 'force-cache' era o padrão
// Next.js 15: cache: 'no-store' é o padrão
const res = await fetch('https://api.example.com/data');
return res.json();
}
// Especificar explicitamente para habilitar o cache
async function getCachedData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache',
next: { revalidate: 3600 }, // 1 hora
});
return res.json();
}
Cache dos Route Handlers
// app/api/data/route.ts
// Next.js 14: GET era cacheado estaticamente
// Next.js 15: Sem cache por padrão
// Habilitar cache
export const dynamic = 'force-static';
// ou
export const revalidate = 3600;
export async function GET() {
const data = await fetchData();
return Response.json(data);
}
Melhorias de Segurança
Segurança das Server Actions
// Proteção de endpoint das Server Actions
// next.config.js
module.exports = {
experimental: {
serverActions: {
// Especificar origens permitidas
allowedOrigins: ['my-app.com', 'staging.my-app.com'],
},
},
};
// app/actions.ts
'use server';
import { headers } from 'next/headers';
export async function sensitiveAction() {
// Verificação de referer
const referer = headers().get('referer');
if (!referer?.startsWith('https://my-app.com')) {
throw new Error('Origem inválida');
}
// Proteção CSRF é feita automaticamente
// ...
}
Melhorias na Exportação Estática
// next.config.js
module.exports = {
output: 'export',
// Otimização de imagens na exportação estática
images: {
unoptimized: true,
// ou usar loader externo
loader: 'custom',
loaderFile: './image-loader.js',
},
};
// image-loader.js
export default function customLoader({ src, width, quality }) {
return `https://cdn.example.com/${src}?w=${width}&q=${quality || 75}`;
}
instrumentation.ts
Permite executar código durante o bootstrap da aplicação.
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Executar apenas no runtime Node.js
await import('./monitoring/sentry');
}
if (process.env.NEXT_RUNTIME === 'edge') {
// Executar apenas no runtime Edge
await import('./monitoring/edge-logger');
}
}
export function onRequestError(error: Error, request: Request) {
// Envio de logs de erro
console.error('Erro na requisição:', error);
}
Migração
# Upgrade
npm install next@15 react@19 react-dom@19
# Codemod automático
npx @next/codemod@latest upgrade latest
Principais Mudanças Quebradas
| Mudança | Next.js 14 | Next.js 15 |
|---|---|---|
| fetch padrão | cache: ‘force-cache’ | cache: ‘no-store’ |
| Route Handler | Cache estático | Sem cache |
| Versão do React | React 18 | React 19 |
| Node.js mínimo | 18.17.0 | 18.18.0 |