Mejores prácticas de renderizado en Next.js
Un resumen condensado de las 25 mejores prácticas más importantes extraídas de cada página de esta sección.
- Usa Server Components por defecto: Cada componente en el App Router es un Server Component a menos que optes por salir con
"use client", así que usa Server Components primero y solo añade la directiva cuando realmente necesites hooks, eventos, o APIs del navegador. - Usa await directamente en Componentes Async: Los Server Components pueden ser
asyncy llamar aawait fetch(...)oawait db.query(...)en el cuerpo de la función — salta los estados de carga deuseEffect, ya que los hooks no están permitidos en el servidor de todas formas. - Serializa Props a través del límite: Las props pasadas de un Server Component a un Client Component deben ser serializables en JSON (strings, números, objetos planos, arrays, Server Actions); las funciones regulares, instancias de clase, Refs, y Symbols fallan silenciosamente o lanzan errores.
- Usa server-only para Secretos:
import "server-only"al inicio de módulos que leenAUTH_SECRET, URLs de base de datos, o claves de firma hace que la compilación falle en el momento en que un Client Component los importa, previniendo fugas al paquete del navegador. - Baja los límites de use client: Marcar un componente de nivel superior como
"use client"arrastra cada hijo y cada utilidad importada al paquete del cliente; extrae solo la parte interactiva (como unLikeButton) en el Client Component y mantén sus padres renderizados por el servidor. - Protege las APIs del navegador en useEffect:
window,document,localStorage, eIntersectionObserverno existen durante SSR, así que accede a ellos dentro deuseEffect(o detrás de una verificacióntypeof window !== "undefined") e inicializa el estado a un valor seguro para el servidor primero. - Evita desincronizaciones de hidratación: Nunca llames a
Date.now(),Math.random(), o leaswindowdurante el render — el HTML del servidor y el re-render del cliente estarán en desacuerdo; mueve valores variables auseEffect, o usasuppressHydrationWarningpara casos deliberados. - Pasa Server Components como children: Un Client Component no puede
importun Server Component (la importación silenciosamente se vuelve solo de cliente), pero puede recibir uno a través dechildreno props JSX nombradas — orquesta la composición desde un padre Server Component. - Aísla Providers en un archivo "use client": React Context requiere un Client Component, pero poner providers en el layout raíz haría que todo fuera cliente; en su lugar, ponlos en
app/providers.tsxcon"use client"y renderiza<Providers>{children}</Providers>desde el layout del Server Component. - Extrae solo la pieza interactiva: En lugar de convertir toda una tarjeta en un Client Component por un botón, mantén la tarjeta como un Server Component y extrae el botón (por ej.,
LikeButton) a su propio archivo pequeño"use client". - Sabe qué activa el renderizado dinámico:
cookies(),headers(),searchParams,connection(), cualquier fetch concache: "no-store", oexport const dynamic = "force-dynamic"hacen que toda la ruta sea renderizada dinámicamente — un único uso en cualquier componente es suficiente. - Usa generateStaticParams para rutas conocidas: Pre-renderiza cada ruta que puedas listar en tiempo de compilación devolviendo sus params de
generateStaticParams, y dejadynamicParams = true(el valor por defecto) así que las rutas nuevas se renderizarán en la primera solicitud y luego se almacenarán en caché. - revalidate: 0 significa siempre dinámico:
export const revalidate = 0es equivalente aforce-dynamic, no "revalidar inmediatamente" — usa un entero positivo (por ej.,revalidate = 60) para ISR y reserva0o"no-store"para datos verdaderamente por solicitud. - No mezcles force-static con funciones dinámicas:
export const dynamic = "force-static"hace que la compilación falle si la página llama acookies()/headers()/searchParams; ya sea remueve la llamada dinámica o retrocede adynamic = "auto". - Habilita PPR de forma incremental: Establece
experimental: { ppr: "incremental" }ennext.config.tsy opta por rutas una a la vez conexport const experimental_ppr = trueasí puedas enviar PPR de forma progresiva y verificar que cada ruta se comporta. - Envuelve hijos dinámicos en Suspense: Los límites de Suspense definen la división cascara-estática-vs-agujero-dinámico; un componente dinámico sin un
<Suspense>circundante arrastra toda la ruta PPR de vuelta a renderizado completamente dinámico. - Consolida agujeros dinámicos: Cada agujero de Suspense cuesta tiempo de computación del servidor en tiempo de solicitud, así que agrupa datos relacionados de tiempo de solicitud en menos límites en lugar de dispersar una docena de pequeños y anular el caché del CDN.
- Siempre proporciona sizes con fill: Un
<Image fill>sin un propsizeshace que el navegador solicite la variante más grande en cada dispositivo; suministra una string consciente de puntos de quiebre como"(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"que coincida con el layout. - Configura remotePatterns: Cualquier host de imagen externa necesita una entrada en
images.remotePatternsennext.config.ts; sin ella, las imágenes remotas lanzan un error de compilación/runtime ynext/imagese niega a optimizarlas. - Reserva priority para imágenes LCP: Añade
prioritysolo a una o dos imágenes LCP encima de la línea de flotación — desactiva la carga perezosa e inyecta un consejo de precarga, así que usarlo en exceso ralentiza la página en lugar de acelerarla. - width/height establece relación de aspecto, no tamaño: Los props
widthyheightennext/imagecierran una relación de aspecto para prevención de CLS; usa CSS (className, tamaño del contenedor) para controlar en qué se renderiza la imagen. - Usa next/font para auto-alojamiento:
next/font/googleynext/font/localdescargan y sirven fuentes desde tu propio origen con encabezados de caché inmutables, eliminando solicitudes externas, FOUT, y CLS a través de métricas de fallback auto-generadas. - Aplica className o variable al DOM: Las fuentes solo se activan cuando asignas
inter.className(ointer.variable) a<html>,<body>, o el contenedor relevante; una clase olvidada es el error más común de "mi fuente no está cargando". - Omite weight para fuentes variables: Pasar
weight: ["400", "700"]explícito para una fuente que tiene una compilación variable fuerza a Next.js a descargar múltiples archivos estáticos en lugar de un archivo variable; eliminaweightcuando la fuente lo soporta para mantener el paquete pequeño. - Prefiere display: swap: Usa
display: "swap"(el valor por defecto recomendado) así se muestre una fuente de fallback inmediatamente y se reemplace cuando la fuente personalizada carga;display: "optional"puede dejar texto invisible si la fuente no llega dentro de ~100ms.