Vision General de Next.js 15
Next.js 15 es un lanzamiento mayor que logra el soporte oficial de React 19 y la estabilizacion de Turbopack. Se han realizado mejoras significativas tanto en la experiencia de desarrollo como en el rendimiento.
flowchart TB
subgraph Next15["Aspectos Destacados de Next.js 15"]
subgraph Turbo["Turbopack (Estable para Dev)"]
T1["Inicio del servidor de desarrollo: 76% mas rapido"]
T2["Fast Refresh: 96% mas rapido"]
T3["Primera compilacion: 45% mas rapido"]
end
subgraph React19["Soporte React 19"]
R1["Server Actions (Estable)"]
R2["Hook use()"]
R3["React Compiler (Experimental)"]
end
subgraph PPR["Partial Prerendering (Beta)"]
P1["Shell estatico + streaming de contenido dinamico"]
end
subgraph Cache["Nuevos Valores por Defecto de Cache"]
C1["fetch: no-store (por defecto)"]
C2["Route Handlers: no-store (por defecto)"]
end
end
Estabilizacion de Turbopack
En Next.js 15, Turbopack para el servidor de desarrollo se ha convertido en version estable.
# Iniciar servidor de desarrollo con Turbopack
next dev --turbo
# El build aun usa Webpack (Turbopack en desarrollo)
next build
Comparacion de Rendimiento
Comparacion de rendimiento en proyecto grande (5000 modulos):
| Elemento | Webpack | Turbopack | Mejora |
|---|---|---|---|
| Inicio del servidor de desarrollo | 8.2s | 2.0s | 76% mas rapido |
| Fast Refresh (1 archivo modificado) | 800ms | 30ms | 96% mas rapido |
| Primera compilacion de ruta | 600ms | 330ms | 45% mas rapido |
| Uso de memoria | 1.6GB | 800MB | 50% de reduccion |
Soporte para 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;
// Validacion
if (!title || title.length < 3) {
return { error: 'El titulo debe tener al menos 3 caracteres' };
}
// Operacion de base de datos
await db.posts.create({ title, content });
// Revalidacion de cache
revalidatePath('/posts');
// Redireccion
redirect('/posts');
}
// Accion con actualizacion optimista
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="Titulo" required />
<textarea name="content" placeholder="Contenido" required />
<button type="submit">Crear 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="Titulo" disabled={isPending} />
<textarea name="content" placeholder="Contenido" disabled={isPending} />
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Creando...' : 'Crear 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)
Una nueva estrategia de renderizado que combina shell estatico con contenido dinamico.
flowchart TB
subgraph BuildTime["Tiempo de Build"]
subgraph StaticShell["Shell Estatico"]
Header["Header (estatico)"]
subgraph Layout["Layout"]
Content["Content (estatico)"]
Suspense["Suspense Fallback<br/>(Skeleton)"]
end
end
end
subgraph RequestTime["Tiempo de Solicitud"]
Step1["1. Devolver shell estatico inmediatamente (TTFB: pocos ms)"]
Step2["2. Streaming de parte dinamica"]
Step3["3. Hidratacion secuencial en limites Suspense"]
Step1 --> Step2 --> Step3
end
BuildTime --> RequestTime
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
// app/products/[id]/page.tsx
import { Suspense } from 'react';
// Parte generada estaticamente
export default function ProductPage({ params }: Props) {
return (
<div>
{/* Contenido estatico */}
<ProductDetails id={params.id} />
{/* Contenido dinamico (streaming) */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations userId={getUserId()} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<RecentReviews productId={params.id} />
</Suspense>
</div>
);
}
// Componente que usa datos dinamicos
async function PersonalizedRecommendations({ userId }: { userId: string }) {
// Esta parte se ejecuta en tiempo de solicitud
const recommendations = await getRecommendations(userId);
return <RecommendationList items={recommendations} />;
}
Nueva Semantica de Cache
En Next.js 15, el comportamiento por defecto del cache ha cambiado.
// Next.js 14 y anteriores: cache por defecto
// Next.js 15: sin cache por defecto
// Cambio en el valor por defecto de fetch API
async function getData() {
// Next.js 14: cache: 'force-cache' era el defecto
// Next.js 15: cache: 'no-store' es el defecto
const res = await fetch('https://api.example.com/data');
return res.json();
}
// Para habilitar cache, especificar explicitamente
async function getCachedData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache',
next: { revalidate: 3600 }, // 1 hora
});
return res.json();
}
Cache de Route Handlers
// app/api/data/route.ts
// Next.js 14: GET se cachea estaticamente
// Next.js 15: sin cache por defecto
// Para habilitar cache
export const dynamic = 'force-static';
// o
export const revalidate = 3600;
export async function GET() {
const data = await fetchData();
return Response.json(data);
}
Mejoras de Seguridad
Seguridad de Server Actions
// Proteccion de endpoints de Server Actions
// next.config.js
module.exports = {
experimental: {
serverActions: {
// Especificar origenes permitidos
allowedOrigins: ['my-app.com', 'staging.my-app.com'],
},
},
};
// app/actions.ts
'use server';
import { headers } from 'next/headers';
export async function sensitiveAction() {
// Verificacion de referer
const referer = headers().get('referer');
if (!referer?.startsWith('https://my-app.com')) {
throw new Error('Origen invalido');
}
// La proteccion CSRF se realiza automaticamente
// ...
}
Mejoras en Exportacion Estatica
// next.config.js
module.exports = {
output: 'export',
// Optimizacion de imagenes en exportacion estatica
images: {
unoptimized: true,
// O usar un 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 ejecutar codigo durante el bootstrap de la aplicacion.
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Ejecutar solo en runtime Node.js
await import('./monitoring/sentry');
}
if (process.env.NEXT_RUNTIME === 'edge') {
// Ejecutar solo en runtime Edge
await import('./monitoring/edge-logger');
}
}
export function onRequestError(error: Error, request: Request) {
// Envio de logs de error
console.error('Error de solicitud:', error);
}
Migracion
# Actualizacion
npm install next@15 react@19 react-dom@19
# Codemod automatico
npx @next/codemod@latest upgrade latest
Principales Cambios Disruptivos
| Cambio | Next.js 14 | Next.js 15 |
|---|---|---|
| fetch por defecto | cache: ‘force-cache’ | cache: ‘no-store’ |
| Route Handler | Cache estatico | Sin cache |
| Version de React | React 18 | React 19 |
| Requisito minimo de Node.js | 18.17.0 | 18.18.0 |