Guía completa de optimización del rendimiento de APIs en Next.js: caché, streaming y edge computing

El viernes a las nueve de la noche, el product manager compartió una captura en el grupo. En el vídeo de prueba, el tester abrió la lista del blog en el móvil: el icono de carga giró cinco segundos enteros y la pantalla seguía en blanco. En la esquina inferior derecha decía: «¿De qué década es esta web?»
Abrí Chrome DevTools y, vaya, la petición a la API tardaba 3200 ms. Me entró un poco de pánico: sabía que esa ruta era lenta, pero no había tenido tiempo de optimizarla; no imaginaba que fuera tan grave.
Luego dediqué dos días a estudiar la optimización de rendimiento en Next.js y descubrí que no es tan complicado. Con la estrategia de caché adecuada, más streaming y edge computing, el tiempo de respuesta bajó de 3 segundos a menos de 500 ms. Lo más importante: entendí cuándo usar cada enfoque — eso importa más que conocer todas las tecnologías.
Hoy repasamos tres palancas: cómo elegir la caché, cómo implementar streaming y en qué casos encajan las Edge Functions. El código está probado en producción y las cifras son reales; puedes usarlo tal cual.
¿Por qué tu API de Next.js va tan lenta?
Empecemos por los cuellos de botella habituales. Al depurar esa API de 3 segundos encontré varios problemas típicos:
Consultas a la base de datos sin optimizar. Había un bucle que, por cada artículo, consultaba al autor por separado: el clásico N+1. Cien artículos, cien peticiones a la base de datos; normal que vaya lento. Peor aún: algunas tablas ni siquiera tenían índices.
Cero caché. Cada vez que el usuario refrescaba, el servidor volvía a consultar la base de datos, recalcular y reformatear. La configuración cambiaba una vez al mes y, aun así, se recalculaba cada segundo.
Devolver todo de golpe. La API devolvía el contenido completo de cien artículos, incluido el cuerpo. La respuesta JSON superaba 2 MB; solo la transferencia ya costaba un segundo. En la lista no hace falta el cuerpo, solo título y resumen.
Ubicación del servidor. Lo teníamos en la costa oeste de EE. UU.; para usuarios en China, ida y vuelta desde 200 ms, más el efecto del GFW… mejor no entrar en detalle.
Los cambios de caché en Next.js 16
En octubre de 2025, Next.js 16 trajo un cambio importante: pasó de caché implícita a caché explícita.
Antes Next.js cacheaba muchas cosas solo, cómodo en teoría, pero en la práctica generaba dudas: ¿esto está cacheado? ¿Cuánto tiempo? ¿Cómo limpiarlo? A veces los datos ya estaban actualizados y la página seguía mostrando lo viejo; tras depurar descubrías que era la caché.
Ahora tienes que decirle explícitamente a Next.js qué cachear y durante cuánto. Un poco más de trabajo, pero al menos sabes qué ocurre y tienes control.
Tres direcciones para optimizar
Con los problemas claros, las palancas también:
- Caché: no repitas lo que ya hiciste
- Streaming: calcula y transmite en paralelo; no esperes a tener todo listo
- Edge computing: acerca el servidor al usuario
Vamos una por una.
Estrategias de caché: elegir bien ahorra trabajo
Next.js tiene cuatro mecanismos de caché: Request Memoization, Data Cache, Full Route Cache y Router Cache. La primera vez que leí la documentación también me perdí.
No hace falta memorizarlos todos. En API Routes, lo más útil es Data Cache — cachear resultados de consultas o respuestas de APIs externas.
Escenario 1: caché de datos estáticos
Configuración del sitio, listas de categorías… datos que casi no cambian. Puedes cachearlos una hora o más.
// app/api/categories/route.js
export async function GET() {
const data = await fetch('https://api.example.com/categories', {
next: { revalidate: 3600 } // Caché de 1 hora
})
return Response.json(await data.json())
}
Así de simple. revalidate: 3600 significa una hora de caché y luego refresco automático.
Escenario 2: caché de datos de usuario
El perfil del usuario no cambia a menudo, pero tampoco puedes servir datos obsoletos para siempre. Aquí encaja stale-while-revalidate:
// app/api/user/profile/route.js
export async function GET(request) {
const user = await getUserFromDB()
return new Response(JSON.stringify(user), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 's-maxage=60, stale-while-revalidate=300'
}
})
}
La idea es inteligente: devuelves la caché (aunque esté un poco caducada) y actualizas en segundo plano. El usuario no nota latencia y los datos no quedan demasiado viejos.
s-maxage=60: la caché es fresca durante 60 segundos. stale-while-revalidate=300: hasta 300 segundos más puedes servir la versión antigua mientras se actualiza en background.
Escenario 3: no cachear datos en tiempo real
Cotizaciones, mensajes de chat… mejor sin caché o con WebSocket / Server-Sent Events.
export async function GET() {
const price = await getStockPrice()
return new Response(JSON.stringify(price), {
headers: {
'Cache-Control': 'no-store' // Sin caché
}
})
}
Invalidación de caché: ¿qué hacer tras actualizar datos?
El usuario actualiza su perfil y la caché sigue mostrando lo antiguo. Hay que invalidarla a mano.
Next.js ofrece revalidateTag y revalidatePath:
// app/api/user/update/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const data = await request.json()
await updateUserProfile(data)
// Invalidar la caché relacionada con el usuario
revalidateTag('user-profile')
return Response.json({ success: true })
}
En la ruta de consulta, etiqueta la caché:
export async function GET() {
const data = await fetch('db-api/user', {
next: {
revalidate: 3600,
tags: ['user-profile'] // Etiqueta
}
})
return Response.json(await data.json())
}
Tras actualizar el perfil, la caché relacionada expira y la siguiente petición trae datos frescos.
Errores habituales
Error 1: caché excesiva. Vi a alguien cachear el estado del pedido una hora; el usuario pagaba y tardaba media hora en ver el estado actualizado. El TTL debe ajustarse al tipo de dato, no cuanto más largo mejor.
Error 2: olvidar el precalentamiento. La primera petición sigue siendo lenta porque la caché está vacía. Tras el despliegue, conviene invocar la ruta una vez para precargar datos calientes.
Error 3: claves de caché mal diseñadas. Se cachean datos del usuario A y el usuario B recibe los de A. La clave debe incluir el ID de usuario u otros discriminadores.
Respuesta en streaming: listas grandes sin bloqueos
La caché evita recalcular, pero a veces el cálculo es lento o el volumen es grande. Ahí entra el streaming.
¿Qué es el streaming?
Una API tradicional es como un restaurante que sirve todos los platos a la vez cuando el más lento está listo. Diez platos, esperas al más tardío.
El streaming sirve plato a plato: el cliente empieza a comer mientras llegan los siguientes. El tiempo total puede ser similar, pero no esperas con la mesa vacía.
Para el usuario, pasa de «pantalla en blanco tres segundos» a «en 500 ms ya veo las primeras filas y puedo navegar». La sensación cambia por completo.
¿Cuándo usar streaming?
Escenarios típicos:
- Listas largas: productos, artículos, resultados de búsqueda
- Contenido generado por IA: el efecto máquina de escribir de ChatGPT es streaming
- Archivos grandes: exportar Excel, generar PDF
- Logs en tiempo real: build logs, progreso de tareas
En general, si hay muchos datos o el cálculo tarda, merece la pena considerarlo.
¿Cómo implementarlo en Next.js?
Lo más habitual es ReadableStream:
// app/api/posts/stream/route.js
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
// Obtener datos por lotes
for (let page = 0; page < 5; page++) {
// 20 registros por lote
const posts = await fetchPostsFromDB({ page, limit: 20 })
// Enviar este lote
const chunk = JSON.stringify(posts) + '\n'
controller.enqueue(encoder.encode(chunk))
// Simular tiempo de procesamiento
await new Promise(r => setTimeout(r, 100))
}
// Fin del envío
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked'
}
})
}
No es complejo. Lo esencial:
- Crear
ReadableStream - En
start, obtener datos por lotes controller.enqueue()por cada lotecontroller.close()al terminar
¿Cómo consumirlo en el frontend?
El cliente también debe leer el stream:
async function fetchStreamData() {
const response = await fetch('/api/posts/stream')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let allPosts = []
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log('Recepción de datos completada')
break
}
// Decodificar
const chunk = decoder.decode(value)
// Parsear JSON (una línea por lote)
const posts = JSON.parse(chunk)
allPosts = [...allPosts, ...posts]
// Actualizar la UI al vuelo
updatePostList(allPosts)
}
}
Al abrir la página, la lista se va llenando poco a poco en lugar de quedarse en blanco.
Comparación de resultados reales
Tras añadir streaming a la lista del blog:
El tiempo total solo mejoró unos 1300 ms, pero la percepción es mucho mayor. A los 500 ms ya puedes interactuar; el resto lo pasas leyendo, no esperando.
Un truco extra
Con volúmenes enormes, combina scroll virtual (Virtual Scrolling): solo renderizas lo visible. Aunque lleguen 1000 filas, la página no se traba.
En React: react-window o react-virtualized. En Vue: vue-virtual-scroller.
Edge Functions: la API en la puerta del usuario
Caché y streaming optimizan en software; hay un enfoque más directo: acercar el servidor al usuario.
El impacto de la distancia física
La latencia de red depende sobre todo de la distancia. La luz tiene límite: un paquete de Pekín a la costa oeste de EE. UU. son al menos 200 ms de ida y vuelta; es física, no hay magia.
Antes solo podías desplegar en un sitio fijo, p. ej. Alibaba Cloud en Pekín: rápido allí, lento en EE. UU.
Las Edge Functions despliegan código en decenas o cientos de nodos; el tráfico va al más cercano. Usuario en Pekín → nodo en Pekín; en Nueva York → nodo en Nueva York. Latencia por debajo de 50 ms.
Edge Runtime vs Node.js Runtime
Las API Routes de Next.js usan por defecto Node.js Runtime: todas las APIs de Node (fs, crypto, conexiones a base de datos, etc.).
Edge Runtime se basa en V8 (el motor de Chrome), no es Node.js completo. Arranque ultrarrápido (0-5 ms), pero muchas APIs de Node no están disponibles.
Comparación rápida:
| Característica | Node.js Runtime | Edge Runtime |
|---|---|---|
| Arranque | 100-500ms | 0-5ms |
| APIs disponibles | Todas las de Node.js | Limitadas (solo Web estándar) |
| Casos de uso | Lógica compleja, base de datos | Lógica ligera, auth, proxy |
| Latencia global | Depende del despliegue | Global <50ms |
| Memoria | Mayor | Menor (128MB) |
¿Qué casos encajan en Edge Functions?
No toda API debe migrar a Edge. Escenarios típicos:
Escenario 1: autenticación
Ideal para Edge: comprobar JWT, validar API key. Lógica ligera en el borde; peticiones inválidas no llegan al servidor central.
// app/api/auth/route.js
export const runtime = 'edge'
export async function GET(request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
// Validar token (p. ej. con jose, compatible con Edge)
const isValid = await verifyToken(token)
if (!isValid) {
return new Response('Invalid token', { status: 401 })
}
return Response.json({ user: 'authenticated' })
}
Escenario 2: personalización geográfica
Según la IP del usuario: idioma, moneda, recomendaciones.
export const runtime = 'edge'
export async function GET(request) {
// Geolocalización (Vercel la inyecta automáticamente)
const country = request.geo?.country || 'US'
const city = request.geo?.city || 'Unknown'
// Contenido según ubicación
const content = getLocalizedContent(country)
return Response.json({
country,
city,
content,
currency: country === 'CN' ? 'CNY' : 'USD'
})
}
Sin base de datos; procesamiento en el borde, muy rápido.
Escenario 3: proxy de API
El frontend llama a varias APIs externas; en Edge puedes agregarlas y reducir peticiones del cliente.
export const runtime = 'edge'
export async function GET(request) {
// Peticiones en paralelo
const [weather, news] = await Promise.all([
fetch('https://api.weather.com/...'),
fetch('https://api.news.com/...')
])
return Response.json({
weather: await weather.json(),
news: await news.json()
})
}
Una petición del usuario; el backend paraleliza y baja la latencia total.
Escenario 4: pruebas A/B
Decidir en el borde qué variante servir sin tocar la app principal.
export const runtime = 'edge'
export async function GET(request) {
const userId = request.headers.get('x-user-id')
// A/B simple
const variant = parseInt(userId) % 2 === 0 ? 'A' : 'B'
const content = variant === 'A' ? getContentA() : getContentB()
return Response.json({ variant, content })
}
Limitaciones de Edge Functions
Si son tan buenas, ¿por qué no migrar todo? Porque hay límites:
Límite 1: APIs exclusivas de Node.js
fs, path, child_process no están disponibles. Si tu código los usa, fallará en Edge.
Límite 2: conexión a base de datos
Conexiones clásicas (pg, mysql2) dependen del módulo net de Node. Usa soluciones HTTP:
- Prisma Data Proxy
- PlanetScale (MySQL)
- Supabase (PostgreSQL)
- Redis (API HTTP)
Límite 3: memoria y tiempo de ejecución
Suele haber límite de 128 MB y 30 s. No encaja para cálculos pesados o datos masivos.
Mi recomendación: uso mixto
No hace falta elegir uno u otro. Yo uso:
- Capa edge: auth, geolocalización, proxy simple
- Capa central (Node.js): lógica de negocio, base de datos, archivos
Edge filtra lo inválido y lo simple; lo complejo va al servidor central. Menos latencia sin renunciar a capacidades.
Mediciones reales
Según un benchmark publicado en Medium:
- Vercel Edge Functions: latencia media 48,3 ms
- Cloudflare Workers (custom): latencia media 36,37 ms
- API Node.js tradicional (una región): latencia media 200-500 ms
Edge es más rápido, pero depende de dónde estén tus usuarios. Si todos están en China, un servidor tradicional en China puede ser mejor.
Caso práctico: optimizar la API de lista de artículos del blog
Juntemos las tres técnicas con el ejemplo del principio: la lista del blog que tardaba tanto.
Problemas antes de optimizar
Código original:
// app/api/posts/route.js
export async function GET() {
// Problema 1: consulta a BD en cada petición, sin caché
const posts = await db.post.findMany({
take: 100,
include: {
author: true, // Problema 2: consulta N+1
tags: true
}
})
// Problema 3: contenido completo, payload enorme
return Response.json(posts)
}
Datos de rendimiento:
- Tiempo de respuesta: 2800 ms
- Tamaño JSON: 2,3 MB
- Experiencia: pantalla en blanco ~3 s
Paso 1: optimizar consultas a la base de datos
Primero el N+1 y solo campos necesarios:
export async function GET() {
const posts = await db.post.findMany({
take: 100,
select: {
id: true,
title: true,
summary: true, // Solo resumen, no cuerpo completo
createdAt: true,
author: {
select: { name: true, avatar: true }
}
}
})
return Response.json(posts)
}
Resultado: 800 ms de respuesta; JSON de 2,3 MB a 180 KB.
Paso 2: añadir caché
La lista cambia poco; caché de 5 minutos:
export async function GET() {
const posts = await db.post.findMany({
// ... igual que arriba
}, {
next: {
revalidate: 300, // Caché 5 minutos
tags: ['posts']
}
})
return Response.json(posts)
}
Al publicar, invalidar caché:
// app/api/posts/publish/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const newPost = await request.json()
await db.post.create({ data: newPost })
// Invalidar caché de la lista
revalidateTag('posts')
return Response.json({ success: true })
}
Resultado: con caché acertada, 50 ms; carga del servidor -90%.
Paso 3: pasar a streaming
Aunque ya va mejor, la primera visita (sin caché) sigue esperando ~800 ms. Streaming:
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const batchSize = 20
for (let page = 0; page < 5; page++) {
const posts = await db.post.findMany({
skip: page * batchSize,
take: batchSize,
select: { /* igual que arriba */ }
})
const chunk = JSON.stringify(posts) + '\n'
controller.enqueue(encoder.encode(chunk))
}
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'application/x-ndjson', // Newline Delimited JSON
'Cache-Control': 's-maxage=300, stale-while-revalidate=600'
}
})
}
Resultado: primer lote en ~300 ms; el usuario navega mientras llega el resto; 800 ms totales pero casi sin espera percibida.
Paso 4: auth en Edge (opcional)
Si hace falta auth, validación inicial en Edge:
// app/api/posts/route.js (capa auth Edge)
export const runtime = 'edge'
export async function GET(request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
// OK: reenviar a la API real (Node.js Runtime)
return fetch(`${process.env.API_BASE_URL}/posts/internal`, {
headers: { authorization: token }
})
}
Peticiones inválidas se cortan en el borde.
Comparación de resultados
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Primera visita | 2800ms | 300ms (primer lote) | 89% ↓ |
| Con caché | - | 50ms | 98% ↓ |
| Tamaño JSON | 2,3MB | 180KB | 92% ↓ |
| Tiempo hasta interacción | 2800ms | 300ms | 89% ↓ |
| Carga del servidor | 100% | 10% | 90% ↓ |
Ya nadie pregunta «¿de qué década es esta web?».
Monitorización y mejora continua
Optimizar no es el final; hay que medir de forma continua.
Métricas clave
Me fijo en estas:
-
Distribución del tiempo de respuesta (P50, P95, P99)
- P50 (mediana): la mitad de los usuarios
- P95: el 95%
- P99: el 1% más lento (a veces anomalías)
-
Tasa de acierto de caché
- <70%: revisar estrategia
- >95%: quizá TTL demasiado largo, datos poco frescos
-
Tasa de error
- No debe subir tras optimizar
- Ojo con fallos a mitad de stream
-
Distribución geográfica
- Latencia por región
- Decide si necesitas Edge Functions
Herramientas de monitorización
Vercel Analytics: si despliegas en Vercel, métricas por API incluidas.
Next.js Instrumentation API (novedad 2026): puntos de medición en código:
// instrumentation.js
export function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
require('./monitoring')
}
}
// monitoring.js
export function onRequestEnd(info) {
console.log(`API ${info.url} took ${info.duration}ms`)
// Enviar a la plataforma de monitorización
sendToMonitoring({
url: info.url,
duration: info.duration,
status: info.status
})
}
Logs propios: simple pero efectivo:
export async function GET() {
const start = Date.now()
const data = await fetchData()
const duration = Date.now() - start
console.log(`API /posts took ${duration}ms`)
return Response.json(data)
}
Recomendaciones de mejora continua
- Revisar la caché con regularidad: el negocio cambia, la estrategia también
- Pruebas A/B: si dudas entre dos enfoques, mídelos
- Decidir con datos reales: no solo intuición; mira el dashboard
La optimización de rendimiento es un proceso continuo, no un proyecto único.
Resumen
En pocas palabras:
Estrategia de caché: según el tipo de dato. Caché larga para estáticos, stale-while-revalidate para usuario, nada para tiempo real. Invalida tras actualizar.
Streaming: cuando hay volumen o cálculo lento. El usuario ve contenido antes; menos pantalla en blanco. Mejor con scroll virtual en el frontend.
Edge Functions: auth, geolocalización, proxy ligero. No sustituyen Node.js para lógica pesada; combínalas.
La optimización es incremental. Empieza por la ruta más lenta, aplica estas tres palancas, mide y ajusta. No busques la perfección de un solo golpe.
Mi lista del blog pasó de 3 s a 300 ms; la diferencia se nota. Elige una API lenta y empieza hoy. Si tienes dudas, comenta; avanzamos juntos.
FAQ
¿Cuándo expira la caché de una API en Next.js?
¿El streaming sirve para todas las APIs?
¿Qué limitaciones tienen las Edge Functions?
¿Cómo elegir la estrategia de caché?
¿Cómo verificar el efecto tras optimizar?
15 min de lectura · Publicado el: 5 ene 2026 · Actualizado el: 21 ago 2026
Guía completa de Next.js
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Guía completa de API Routes en Next.js: de Route Handlers a las mejores prácticas de manejo de errores
Guía completa de API Routes en Next.js: creación de Route Handlers, técnicas de peticiones, mejores prácticas de errores y diseño de respuestas para desarrollar backends con Next.js con soltura.
Parte 18 de 51
Siguiente
Autenticación y seguridad de APIs en Next.js: guía completa desde JWT hasta rate limiting
Guía completa de seguridad de APIs en Next.js: autenticación JWT, configuración CORS, rate limiting y validación de entrada. Aprende a construir aplicaciones de producción seguras con métodos actuales de prevención de vulnerabilidades.
Parte 20 de 51



Comentarios
Inicia sesión con GitHub para dejar un comentario