En las arquitecturas tradicionales Single Page Application (SPA), la obtención de datos se ejecutaba en el lado del cliente utilizando hooks useEffect o librerías de cliente como TanStack Query y SWR.
Este enfoque generaba cuellos de botella críticos de rendimiento:
El App Router de Next.js transforma este paradigma trasladando el data fetching al servidor. Los Server Components consultan bases de datos y microservicios directamente dentro de la red local del centro de datos con latencias inferiores a 1–2 ms. El cliente recibe HTML completamente generado junto con un flujo de streaming, el código de los controladores de base de datos nunca viaja al navegador y las claves secretas de API permanecen estrictamente resguardadas en el entorno del backend.
En Next.js App Router, la obtención de datos se articula en torno a la API nativa fetch y a los componentes asíncronos de servidor (Async Server Components). Next.js complementa el estándar web fetch con un modelo de caché de dos niveles:
Inspeccione el flujo de peticiones a través de Router Cache, Full Route Cache, Memoization y Data Cache
GET /catalog HTTP/1.1 (Sin caché previa)El usuario entra por primera vez con la caché vacía. La petición consulta Full Route Cache y Data Cache en el servidor.
Un Server Component ejecuta una llamada fetch() o una consulta a base de datos.
React examina la memoria RAM del render actual. Si coinciden la URL y los parámetros, devuelve el resultado sin operaciones I/O.
Si no existe en memoria, Next.js consulta la caché persistente en disco/CDN cotejando tags y tiempos de revalidate.
Si la caché no existe o se marca como obsoleta (Stale), se emite una petición física real a la fuente de datos.
La respuesta se guarda en Data Cache y se envía al árbol de componentes para generar los nodos virtuales.
¿Qué ocurre si tres Server Components independientes (Header, ProfileCard, ActivityFeed) en la misma página invocan fetch('https://api.example.com/user', { cache: 'no-store' })?
fetch()#En Next.js 15+, las llamadas fetch() no se almacenan en caché por defecto (cache: 'no-store'). Para habilitar la caché, es necesario definir explícitamente la estrategia en las opciones:
import { notFound } from 'next/navigation';
interface User {
id: number;
name: string;
email: string;
}
export default async function UsersPage() {
// A partir de Next.js 15, fetch() no se cachea por defecto (no-store).
// Para activar la caché, utiliza 'force-cache' o next: { revalidate: N }
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
next: {
revalidate: 3600, // Time-based ISR: caché durante 1 hora
tags: ['users-list'] // Etiqueta para revalidación bajo demanda
}
});
if (!res.ok) {
if (res.status === 404) notFound();
throw new Error('Error al cargar la lista de usuarios');
}
const users: User[] = await res.json();
return (
<main className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-4">Directorio de usuarios</h1>
<ul className="divide-y divide-border">
{users.map((user) => (
<li key={user.id} className="py-2.5 flex justify-between">
<span className="font-medium text-text-primary">{user.name}</span>
<span className="text-text-muted">{user.email}</span>
</li>
))}
</ul>
</main>
);
}React memoriza de forma transparente las llamadas fetch idénticas dentro del árbol de componentes de un mismo renderizado. Ya no es necesario propagar datos vía prop drilling desde un Layout hacia una Page:
// lib/api/users.ts
export async function getUser(id: string) {
// ¡Esta petición fetch se deduplica automáticamente mediante React!
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 60 }
});
return res.json();
}
// app/users/[id]/layout.tsx (Server Component)
interface LayoutProps {
params: Promise<{ id: string }>; // ¡En Next.js 15+, los parámetros son estrictamente asíncronos!
children: React.ReactNode;
}
export default async function UserLayout({ params, children }: LayoutProps) {
const { id } = await params;
const user = await getUser(id); // Petición 1: se ejecuta a través de la red
return (
<div>
<header className="border-b border-border p-4">Usuario: {user.name}</header>
{children}
</div>
);
}
// app/users/[id]/page.tsx (Server Component)
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getUser(id); // Petición 2: ¡se resuelve al instante desde Request Memoization!
return <main className="p-4">Email: {user.email}</main>;
}React.cache() para consultas directas a bases de datos#La deduplicación automática solo opera sobre fetch(). Si consultas la base de datos directamente mediante un ORM (Prisma, Drizzle), envuelve la función con React.cache():
import 'server-only';
import { cache } from 'react';
import { db } from '@/lib/db';
// React.cache deduplica consultas a la base de datos dentro del mismo render
export const getUserById = cache(async (id: string) => {
// Se ejecuta exactamente una vez, aunque se llame en 5 componentes de la misma página
return await db.user.findUnique({
where: { id },
select: { id: true, name: true, email: true }
});
});unstable_cache()#React.cache() almacena datos únicamente en la memoria de un solo render. Para preservar resultados de consultas en la Data Cache persistente entre diferentes usuarios y peticiones, utiliza unstable_cache():
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
export const getPopularPosts = unstable_cache(
async (limit: number) => {
return await db.post.findMany({
orderBy: { views: 'desc' },
take: limit,
});
},
['popular-posts-cache-key'], // Clave identificadora de la caché
{
revalidate: 3600, // Revalidar cada hora
tags: ['posts-summary'] // Etiqueta para revalidación con revalidateTag('posts-summary')
}
);Promise.all#Para erradicar cascadas de red innecesarias, inicia operaciones asíncronas independientes en paralelo:
import { getPost, getComments } from '@/lib/dal/posts';
interface PageProps {
params: Promise<{ id: string }>; // Estándar Next.js 15+
}
export default async function PostPage({ params }: PageProps) {
const { id } = await params;
// 1. Iniciamos las peticiones concurrentemente SIN await
const postPromise = getPost(id);
const commentsPromise = getComments(id);
// 2. Esperamos ambas promesas simultáneamente
const [post, comments] = await Promise.all([postPromise, commentsPromise]);
return (
<article className="p-8 max-w-3xl mx-auto space-y-6">
<h1 className="text-3xl font-bold">{post.title}</h1>
<p className="text-text-muted">{post.body}</p>
<section className="border-t border-border pt-6">
<h2 className="text-xl font-bold mb-4">Comentarios ({comments.length})</h2>
{/* Renderizado de comentarios */}
</section>
</article>
);
}use())#Cuando una consulta presenta latencia considerable (como métricas analíticas o comentarios pesados), no bloquees el renderizado inicial de la página. Pasa la promesa sin resolver al componente de cliente:
import { Suspense } from 'react';
import { getSlowAnalyticsPromise } from '@/lib/dal/analytics';
import { AnalyticsChart } from './AnalyticsChart';
export default async function DashboardPage({
params
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Iniciamos la consulta lenta sin await
const analyticsPromise = getSlowAnalyticsPromise(id);
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Analítica del proyecto #{id}</h1>
<Suspense fallback={<div className="h-64 bg-bg-elevated animate-pulse rounded-xl" />}>
{/* Pasamos la promesa al cliente: se desenvolverá con el hook use() */}
<AnalyticsChart promise={analyticsPromise} />
</Suspense>
</div>
);
}'use client';
import { use } from 'react';
interface Metric {
date: string;
views: number;
}
export function AnalyticsChart({ promise }: { promise: Promise<Metric[]> }) {
// El hook use() de React 19 desenvuelve la promesa recibida por streaming
const data = use(promise);
return (
<div className="border border-border rounded-xl p-4 bg-bg-surface">
<div className="text-sm text-text-muted">Visualizaciones totales: {data.length}</div>
</div>
);
}¿Por qué pasar un objeto literal del tipo getUser({ id: '123' }) a una función envuelta con React.cache() impide la memorización y desencadena consultas duplicadas a la base de datos?
En Next.js 15+, el acceso a las propiedades params y searchParams se convirtió en asíncrono para garantizar la total compatibilidad con Partial Prerendering:
// ❌ ERROR ANTERIOR: la desestructuración síncrona provocará fallos en Next.js 15+
// export default async function Page({ params }: { params: { id: string } }) {
// const user = await getUser(params.id);
// }
// ✅ ESTÁNDAR (Next.js 15+):
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getUser(id);
}La función React.cache() evalúa los argumentos por referencia (Object.is). Si envías a una función memorizada un objeto literal { id: '123' }, en cada invocación se creará un objeto nuevo en el heap, la memorización no surtirá efecto y la consulta se repetirá innecesariamente.
Solución: Transfiere siempre únicamente tipos escalares primitivos (strings, números, booleanos) a funciones protegidas con React.cache().
// ❌ ERROR: pasar un objeto literal anula la deduplicación de React.cache()
// const getUser = cache(async (options: { id: string }) => { /* ... */ });
// await getUser({ id: '123' }); // Llamada 1: Objeto A en heap
// await getUser({ id: '123' }); // Llamada 2: Objeto B en heap, ¡fallo de caché!
// ✅ CORRECTO: pasar únicamente primitivos escalares
const getUser = cache(async (id: string) => { /* ... */ });
await getUser('123'); // La deduplicación opera con total precisiónIntentar consultar un Route Handler propio mediante fetch('http://localhost:3000/api/users') desde un Server Component genera una petición HTTP adicional innecesaria hacia el mismo servidor, obliga a indicar hosts absolutos y falla dentro de contenedores aislados sin enrutamiento loopback.
Solución: Traslada la lógica a una capa de servicios (DAL) e invoca la función de base de datos directamente desde el Server Component.
Cuando un componente recupera un registro del ORM y lo transfiere íntegro a un Client Component, todos los atributos ocultos (hashes de contraseñas, tokens de autenticación, notas privadas) se serializan en el RSC Flight Payload abierto.
Solución: Filtra siempre explícitamente los campos a nivel de consulta (select: { id: true, name: true }) o aprovecha la React 19 Taint API (experimental_taintObjectReference).
Crea un directorio aislado lib/dal/ protegido con el paquete server-only:
// lib/dal/users.ts
import 'server-only';
import { cache } from 'react';
import { db } from '@/lib/db';
import { verifySession } from '@/lib/auth';
// 1. React.cache() garantiza la deduplicación en el ciclo de render
export const getCurrentUser = cache(async () => {
// 2. Punto único para verificación de sesión
const session = await verifySession();
if (!session.isAuth) return null;
// 3. Proyección segura: excluimos passwordHash de la consulta
const user = await db.user.findUnique({
where: { id: session.userId },
select: { id: true, name: true, email: true, role: true }
});
return user;
});Ahora getCurrentUser() puede invocarse con total seguridad en layout.tsx, page.tsx o en cualquier componente subordinado:
React.cache().'server-only'.