Dónde te cuesta de verdad `use client`
El límite de cliente no es un ajuste de rendimiento, es un límite de bundle. Así encuentras hacia dónde ha derivado el tuyo, y qué vale devolverlo a su sitio.
use client no significa «este componente se ejecuta en el cliente». Todos
los componentes de una aplicación Next.js se renderizan en el servidor al
menos una vez.
Lo que use client declara en realidad es un límite: de este módulo hacia
abajo, todo se compila dentro del bundle de JavaScript y se envía al navegador
para que pueda hidratar. Ese es el coste. No el renderizado — el envío.
En cuanto asumes esa definición, los errores habituales se vuelven obvios.
La deriva hacia arriba
Los límites derivan hacia la raíz, una decisión razonable cada vez.
Diseño pide un desplegable en la cabecera. La cabecera necesita useState,
así que use client se pone al principio de Header.tsx. La cabecera importa
Nav, que importa NavItem, que importa una utilidad formatDate que
importa una librería de fechas. Nada de eso necesitaba ser interactivo. Todo
eso está ahora en el bundle, en cada página, para cada visitante.
Seis meses después el layout es un componente de cliente y nadie recuerda por qué.
Encontrar tu límite real
La salida del build te dice dónde se torció. Mira el First Load JS por ruta:
Route (app) Size First Load JS
┌ ○ / 1.2 kB 184 kB
├ ○ /about 0.8 kB 184 kB
└ ○ /pricing 2.1 kB 186 kB
Cuando todas las rutas cargan la misma base grande, el peso está en el layout compartido, no en las páginas. Esa es la firma de un límite de cliente que ha trepado hasta el árbol del layout.
Para el detalle, ejecuta el analizador de bundles:
npm install --save-dev @next/bundle-analyzer
ANALYZE=true npm run buildBuscas dos cosas: librerías que no esperabas, y librerías que aparecen en el chunk compartido cuando deberían estar en el chunk de una sola ruta.
Empujar el límite hacia abajo
La solución tiene casi siempre la misma forma — la hoja interactiva sigue siendo un componente de cliente, y todo lo que la rodea se queda en el servidor.
En lugar de esto:
'use client';
import { heavyMarkdownRenderer } from 'some-large-lib';
export function Article({ content, comments }) {
const [showComments, setShowComments] = useState(false);
return (
<article>
{heavyMarkdownRenderer(content)}
<button onClick={() => setShowComments(true)}>Comments</button>
{showComments ? <Comments data={comments} /> : null}
</article>
);
}haz esto:
// Article.tsx — server component, no 'use client'
import { heavyMarkdownRenderer } from 'some-large-lib';
import { CommentsToggle } from './CommentsToggle';
export function Article({ content, comments }) {
return (
<article>
{heavyMarkdownRenderer(content)}
<CommentsToggle>
<Comments data={comments} />
</CommentsToggle>
</article>
);
}// CommentsToggle.tsx — the only client component
'use client';
export function CommentsToggle({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Comments</button>
{open ? children : null}
</>
);
}La librería de markdown se queda en el servidor. Comments sigue siendo un
componente de servidor aunque se renderice dentro de uno de cliente — porque
se pasa como children, ya se renderizó en el servidor y llega como salida
serializada, no como código.
Los children que se pasan a través de un componente de cliente no cruzan el límite. Esa única regla resuelve la mayoría de los casos en que un equipo cree estar obligado a irse al cliente.
Por qué esto aparece en el INP, no solo en el tiempo de carga
Interaction to Next Paint mide cuánto tarda el hilo principal en responder cuando una visitante toca algo. Cada kilobyte de JavaScript del bundle se parsea, compila y ejecuta antes de que la página pueda responder a nada, y la hidratación compite con ese primer toque.
En un Android de gama media — que es el dispositivo de la mayor parte del mundo — 300 KB de JavaScript son aproximadamente un segundo de trabajo en el hilo principal antes de que termine la hidratación. Un toque durante ese segundo se encola. Eso es un INP fallido, y ninguna cantidad de trabajo en CSS lo arregla.
Los Server Components no son principalmente una optimización de renderizado. Son una manera de no enviar el código en primer lugar.
Cosas que no necesitan ser componentes de cliente
De las auditorías que hacemos, los falsos positivos recurrentes:
- Formatear fechas y números.
Intlfunciona en el servidor. Si lo que preocupa es la zona horaria de la visitante, pasa la cadena ya formateada, o renderiza un elemento<time>con atributodateTimey deja la presentación a CSS o a un componente hoja diminuto. - Leer un tema. El atributo
data-thememás CSS lo resuelve sin JavaScript. - Pedir datos al montar. Si no son específicos del usuario, pídelos en el servidor. Si lo son, pídelos en el servidor detrás de Suspense.
- Analítica. Cárgala en un
<Script>constrategy="afterInteractive"o"lazyOnload", no dentro de un componente. - Librerías de iconos. Los iconos son marcado. Importa el SVG concreto, no el fichero barril que arrastra dos mil.
La regla de revisión que usamos
En toda base de código en la que trabajamos, use client se trata como se
trata un prefijo dangerously: está permitido, a veces es lo correcto, y
nunca pasa una revisión sin una frase que explique por qué el límite
corresponde a ese fichero exacto y no a uno más abajo.
Esa sola convención vale más que cualquier herramienta de tamaño de bundle, porque atrapa el problema en el momento en que se introduce y no dieciocho meses después, cuando alguien por fin abre el analizador.
