Saltar al contenido

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.

7 min de lectura

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 build

Buscas 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. Intl funciona 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 atributo dateTime y deja la presentación a CSS o a un componente hoja diminuto.
  • Leer un tema. El atributo data-theme má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> con strategy="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.

La convención solo funciona si todos comparten el mismo modelo de qué es un componente de servidor, así que lea el modelo de renderizado una vez, bien. La mitad de las fronteras que eliminamos resultan ser formularios, y ahí una server action elimina el componente de cliente en vez de encogerlo. Si el bundle ya es grande y la pregunta es qué fronteras mover primero, eso lo medimos.

Preguntas relacionadas

¿use client significa que el componente solo corre en el navegador?
No. Marca dónde empieza el bundle de cliente. Todo lo que se importe por debajo va al navegador, y el propio componente sigue renderizándose en el servidor para el HTML inicial.
¿Cómo encontramos por dónde se desplazó la frontera?
Con el analizador de bundles, leído como grafo de importaciones en vez de como treemap. Se busca ese componente de cliente cerca de la raíz de un árbol que arrastró consigo todo lo que colgaba de él.
¿Puede haber un componente de servidor dentro de uno de cliente?
Sí, pasándolo como children o como prop. Es el patrón que mantiene la frontera baja, y el que la mayoría de los equipos no usa.
¿Un proveedor de contexto en la raíz es un problema?
Es la causa más común de una frontera demasiado alta. Envuelva el subárbol más pequeño que necesite el contexto, y si de verdad es toda la aplicación, mantenga el proveedor delgado para que cruce poco.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto