Inicio / Artículos / Rendimiento del frontend: desde los puntos ciegos en las revisiones de código hasta las métricas del producto

Rendimiento del frontend: desde los puntos ciegos en las revisiones de código hasta las métricas del producto

Aprenda por qué no basta con superar la revisión de código, cuáles son los Core Web Vitals que realmente importan, y cómo medir y solucionar los problemas reales de rendimiento en React.

2316 palabras

Superó la revisión de código

Su solicitud de integración está en orden. La lógica es correcta. Todas las pruebas han pasado. Un ingeniero senior la aprobó.

Lanza el cambio una tarde de jueves.

Por la mañana del viernes, recibe un mensaje del gerente de producto: “La gente dice que la app se siente lenta.”

Así que abre Chrome DevTools. En su MacBook Pro, conectado a fibra óptica en casa y con Chrome sin activadas las docenas de extensiones, todo parece funcionar sin problemas.

Pero sus usuarios no tienen su misma configuración.

Muchos de ellos usan dispositivos Android de gama media de hace unos años, con conexión 4G que a menudo cae a 3G. Pueden estar en Yakarta, Lagos o Bakú: lugares donde la latencia de red sola puede añadir entre 200 y 400 milisegundos a cada solicitud.

Su app los obliga a esperar sentados.

Por qué existe esta brecha

La mayoría de los desarrolladores front-end trabajan en condiciones casi perfectas y lanzan sus aplicaciones en entornos mucho más caóticos. Esa discrepancia es precisamente donde surgen los problemas de rendimiento.

Aquí está lo que su entorno de desarrollo cotidiano le oculta:

Ralentización de la CPU. Su máquina de desarrollo cuenta con una gran capacidad de procesamiento. Las Herramientas de Desarrollo de Chrome pueden simular una desaceleración de la CPU de 4 a 6 veces, pero casi nadie se molesta en activarla.

Condiciones de red. Probar con localhost implica cero latencia. Los usuarios reales se enfrentan a tiempos de ida y vuelta que van desde 100 hasta 500 milisegundos. Una carga de datos que parece instantánea en su máquina puede congelar la interfaz durante un segundo entero una vez que está en producción.

Tamaño del paquete. Añadir una biblioteca mientras se programa parece algo gratuito, sin costo visible. Sin embargo, en producción esa misma dependencia puede añadir 80 KB a tu paquete inicial, cantidad que un usuario con conexión 3G debe descargar antes de que cualquier cosa se muestre.

Tiempo de análisis de JavaScript. Llevar el paquete al dispositivo es solo el primer paso. El navegador luego debe analizarlo y ejecutarlo. En hardware más débil, el análisis de un paquete de 500 KB por sí solo puede consumir de 3 a 4 segundos.

Juntos, estos factores generan una verdadera desconexión entre cómo percibes tu aplicación tú y cómo la perciben quienes realmente la usan; esa desconexión suele permanecer invisible hasta que una queja la hace evidente.

Por qué el rendimiento es una decisión de producto

Los ingenieros frontend suelen clasificar el rendimiento como un “detalle técnico”. Los gerentes de producto a menudo lo ignoran por completo hasta que se convierte en una emergencia.

Ninguno de estos enfoques es viable.

El rendimiento debe formar parte de las conversaciones sobre el producto, ya que influye en los resultados que realmente sigue la empresa.

Ingresos. Amazon ha informado que cada 100 milisegundos adicionales de latencia les cuestan aproximadamente el 1% en ventas. Con ingresos de mil millones de dólares al día, eso equivale a 10 millones de dólares por cada 100 ms. Las empresas más pequeñas presentan cifras absolutas menores, pero la relación se mantiene.

Retención. Más de la mitad de los visitantes móviles —el 53%— abandonan una página que tarda más de 3 segundos en cargarse. Rara vez presentan quejas; simplemente se van y nunca regresan.

SEO. Desde 2021, Google ha incorporado los Core Web Vitals en su algoritmo de clasificación. Una experiencia lenta hace que su sitio baje en la página de resultados, lo que significa que menos personas lo descubren.

Accesibilidad. La velocidad también es un problema de equidad. Las personas que utilizan dispositivos antiguos y conexiones más lentas están desproporcionadamente concentradas en mercados emergentes y grupos de bajos ingresos. Una aplicación lenta, en efecto, excluye a parte de su audiencia.

Cuando orienta la conversación hacia los ingresos, la retención, la visibilidad en búsquedas y la accesibilidad, el rendimiento deja de parecer algo opcional y pasa a considerarse un requisito básico.

Las métricas que realmente importan

No se puede solucionar lo que no se mide, y no se puede medir lo que no se ha definido. Ahí es donde un vocabulario compartido para el rendimiento se vuelve esencial.

Los Core Web Vitals de Google ofrecen actualmente el marco más fiable para esto.

LCP — Largest Contentful Paint

Este indicador mide cuánto tiempo tarda en renderizarse el elemento más grande visible en la página; en otras palabras, indica cuándo el usuario siente que la página se ha “cargado”.

Bueno: menos de 2,5 segundos. Necesita mejora: entre 2,5 y 4 segundos. Pobre: más de 4 segundos.

Causas típicas: imágenes demasiado grandes y no optimizadas, recursos que bloquean el renderizado y respuestas lentas del servidor.

INP — Interaction to Next Paint

Mide el retraso entre una acción del usuario —un clic, toque o tecla— y el momento en que la pantalla responde visualmente. Sustituyó a FID (First Input Delay) como métrica estándar de interactividad en 2024.

Bueno: menos de 200 ms. Necesita mejora: entre 200 y 500 ms. Pobre: más de 500 ms.

Causas típicas: cálculos intensivos en el hilo principal y tareas síncronas que bloquean la renderización.

CLS — Desplazamiento acumulativo de layout

Mide cuánto se mueve inesperadamente el contenido mientras la página se carga. Las puntuaciones van de 0 (sin desplazamiento) hacia arriba, siendo cualquier valor superior a 1 considerado grave.

Bueno: menos de 0,1. Necesita mejora: entre 0,1 y 0,25. Pobre: más de 0,25.

Causas típicas: imágenes que carecen de ancho y alto explícitos, contenido insertado dinámicamente después de la carga, y fuentes web que se cargan tarde.

Cómo medirlo: Su conjunto de herramientas

Lighthouse (empiece aquí)

Abra las Herramientas de desarrollo de Chrome, cambie a la pestaña Lighthouse y ejecute una auditoría en un perfil móvil con el limitador de rendimiento activado.

Lighthouse devuelve una puntuación entre 0 y 100 para Rendimiento, Accesibilidad, SEO y Buenas prácticas. Lo que lo hace realmente útil es que explica por qué obtuvo esa puntuación y le indica qué corregir primero.

# Or run it from the CLI for CI/CD integration
npm install -g lighthouse
lighthouse https://yourapp.com --output html --output-path report.html

Importante: ejecute Lighthouse siempre en una ventana anónima. Las extensiones del navegador instaladas pueden distorsionar los resultados.

Profiler de las Herramientas de desarrollo de React

Se trata, sin duda, de la herramienta menos utilizada por los desarrolladores de React, a pesar de ser una de las más reveladoras.

Para usarla, abra las Herramientas de desarrollo de React, cambie a la pestaña Profiler, haga clic en Grabar, interactúe con su aplicación y luego detenga la grabación.

El resultado es un gráfico de llamas que muestra cada renderizado que ocurrió: qué componentes se activaron, qué los provocó y cuánto tiempo tardó cada uno.

What to look for:
- Components rendering more than they should
- Renders triggered by unrelated state changes
- Expensive components re-rendering on every keystroke

Biblioteca Web Vitals

Si desea registrar el rendimiento de su aplicación para visitantes reales en lugar de en una sesión local de DevTools, la biblioteca web-vitals es la solución:

import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(metric => {
  // Send to your analytics service
  console.log('LCP:', metric.value);
});onINP(metric => {
  console.log('INP:', metric.value);
});onCLS(metric => {
  console.log('CLS:', metric.value);
});

Este enfoque le proporciona datos recopilados de sesiones reales de usuarios, y no números generados en condiciones artificiales de laboratorio.

El renderizado adicional del que no era consciente

Uno de los problemas de rendimiento más insidiosos en React no se manifiesta abiertamente. Por lo general, se presenta de la siguiente manera:

// ❌ Problem: selecting the full user object
function Header() {
  const user = useSelector(state => state.user);
  return <div>{user.name}</div>;
}

Ese componente se volverá a renderizar cada vez que cambie cualquiera de las propiedades dentro de state.user, independientemente de si el componente realmente lee esa propiedad. Si el objeto del usuario contiene veinte campos y cinco de ellos cambian con frecuencia, Header terminará volviéndose a renderizar cinco veces más de lo necesario.

// ✅ Fix: select only what you need
function Header() {
  const name = useSelector(state => state.user.name);
  return <div>{name}</div>;
}

Con este cambio, Header solo reacciona a los cambios en name. Es una modificación de una sola línea, pero el impacto en el rendimiento es real.

La misma lógica se aplica a Context:

// ❌ Problem: consuming the full context
function ThemeButton() {
  const { theme, user, notifications } = useAppContext();
  return <button className={theme}>Click</button>;
}
// ✅ Fix: split contexts by update frequency
const ThemeContext = createContext();
const UserContext = createContext();function ThemeButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click</button>;
}

Carga diferida: Deje de enviar código que los usuarios no necesitan

Un error común en los proyectos React es enviar todo el paquete de la aplicación en la primera carga de página, incluido el código para rutas que el visitante aún no ha abierto y posiblemente nunca abrirá.

// ❌ Problem: all routes loaded upfront
import CheckoutPage from './pages/CheckoutPage';
import AdminDashboard from './pages/AdminDashboard';
import SettingsPage from './pages/SettingsPage';
function App() {
  return (
    <Routes>
      <Route path="/checkout" element={<CheckoutPage />} />
      <Route path="/admin" element={<AdminDashboard />} />
      <Route path="/settings" element={<SettingsPage />} />
    </Routes>
  );
}
// ✅ Fix: lazy load each route
import { lazy, Suspense } from 'react';
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/checkout" element={<CheckoutPage />} />
        <Route path="/admin" element={<AdminDashboard />} />
        <Route path="/settings" element={<SettingsPage />} />
      </Routes>
    </Suspense>
  );
}

Con esta configuración, cada ruta se convierte en un bloque independiente, de modo que los usuarios solo descargan el código necesario para la página que actualmente están viendo. En aplicaciones más grandes, este cambio por sí solo puede reducir el paquete inicial entre un 40 y un 60 por ciento.

Cuándo NO optimizar: La trampa de useMemo

La mayoría de las guías de rendimiento omiten esta parte: optimizar demasiado pronto puede perjudicar activamente tu código.

useMemo y useCallback conllevan su propio costo: asignar memoria y comparar dependencias en cada renderizado. Si los utilizas en el lugar incorrecto, podrías terminar con un rendimiento incluso peor que antes.

// ❌ Unnecessary — this calculation is not expensive
function UserCard({ user }) {
  const displayName = useMemo(
    () => `${user.firstName} ${user.lastName}`,
    [user.firstName, user.lastName]
  );
  return <div>{displayName}</div>;
}
// ✅ Just compute it — string concatenation is instant
function UserCard({ user }) {
  const displayName = `${user.firstName} ${user.lastName}`;
  return <div>{displayName}</div>;
}
// ✅ useMemo IS worth it here — genuinely expensive calculation
function DataGrid({ rows, filters }) {
  const filteredRows = useMemo(
    () => rows.filter(row => matchesAllFilters(row, filters)),
    [rows, filters]
  );
  return <Table rows={filteredRows} />;
}

El principio rector: realiza un análisis de rendimiento antes de tocar cualquier cosa, optimiza solo después y mide nuevamente para confirmar que la solución fue efectiva. No confíes en tu intuición.

Si el analizador nunca marca un componente como cuello de botella, déjelo sin memorizar: la complejidad adicional no vale la pena.

Optimización de imágenes: la solución más sencilla

Las imágenes suelen ser la causa principal de las lentas cargas de página, pero también están entre los problemas más fáciles de solucionar.

// ❌ Unoptimized: full-size image, no lazy loading
<img src="/hero-image.png" />
// ✅ Optimized: modern format, explicit dimensions, lazy loading
<img
  src="/hero-image.webp"
  width={1200}
  height={600}
  loading="lazy"
  decoding="async"
  alt="Hero image"
/>

Algunos hábitos marcan una gran diferencia:

Cambie de PNG o JPEG a WebP o AVIF. WebP suele reducir el tamaño del archivo entre un 25 y un 35 por ciento en comparación con JPEG manteniendo una calidad similar. AVIF comprime aún más, aunque el soporte de los navegadores para él aún no es universal.

Siempre declare explícitamente los atributos de ancho y alto. Hacerlo evita que el navegador modifique el diseño una vez que la imagen se ha cargado, lo cual ayuda directamente a mejorar su puntuación CLS.

Aplique loading="lazy" a todo lo que se encuentre debajo del despliegue inicial. El navegador esperará a cargar esa imagen hasta que el usuario esté a punto de desplazarla para verla.

Agregue decoding="async" a las imágenes que no son cruciales para la primera renderización. Esto permite al navegador decodificar la imagen en un hilo separado del principal, evitando así bloquear el proceso de renderizado.

Los verdaderos compromisos

Ninguna técnica de rendimiento es gratuita. A continuación, se presenta un análisis honesto de lo que está sacrificando:

Dividir el código por ruta reduce el paquete inicial, pero añade un pequeño retraso la primera vez que el usuario navega a una nueva ruta. Cargar las imágenes de forma perezosa acelera la carga inicial, pero hace que las imágenes aparezcan visiblemente a medida que el usuario desplaza la página. Encerrar cálculos costosos en useMemo disminuye las actualizaciones de la interfaz, pero a costa de una mayor complejidad del código y menor legibilidad. El almacenamiento en caché a través de un Service Worker hace que las visitas repetidas sean casi instantáneas, pero introduce una lógica complicada para invalidar la caché. La renderización del lado del servidor o la generación estática permiten una carga rápida y un mejor SEO, pero requieren más infraestructura en el servidor y añaden complejidad en el proceso de hidratación.

El principio subyacente a todo esto: nunca optimice para una métrica que aún no haya medido realmente.

Una puntuación de Lighthouse de 95 no indica nada sobre si los usuarios reales tienen una buena experiencia. Equipa tu aplicación con la biblioteca Web Vitals, recopila datos de los visitantes reales, localiza el verdadero cuello de botella, corrige ese problema específico y luego mide nuevamente para confirmar que funcionó.

Una lista de verificación práctica

Antes de lanzar cualquier función importante, revisa esta lista:

Performance Checklist
─────────────────────
□ Run Lighthouse on mobile with throttling (target score: 90+)
□ Check bundle size with webpack-bundle-analyzer or source-map-explorer
□ Verify all routes are lazy loaded
□ Confirm images use WebP/AVIF with explicit dimensions
□ Profile with React DevTools — no unnecessary re-renders
□ Check Core Web Vitals in production with web-vitals library
□ Test on a real mobile device, not just DevTools emulation
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Or for Vite
npm install --save-dev rollup-plugin-visualize

Conclusión

El trabajo de rendimiento no es un esfuerzo final que se realiza justo antes del lanzamiento, ni es una capa que se añade una vez que una función ya funciona. Es una disciplina: un conjunto de hábitos y herramientas integrados en tu flujo de trabajo diario.

Los ingenieros que crean las aplicaciones más rápidas no son necesariamente más talentosos que aquellos que crean aplicaciones lentas. Simplemente miden de manera más consistente. Saben qué herramienta se adapta a cada problema y han interiorizado un ciclo sencillo: primero hacer un perfil, corregir solo lo que indican los datos y luego medir de nuevo para confirmar que eso ayudó.

Una aplicación puede superar todas las revisiones de código y, aun así, dejar frustrados a los usuarios reales.

Mide. Haz un perfil. Corrige lo que realmente importa.

Lecturas relacionadas

  • Evitando errores de estado silencioso por mutación de referencias en JavaScript — Aprenda por qué modificar objetos y arrays mediante referencias interrumpe las actualizaciones de React, por qué la operación spread solo realiza copias superficiales, y cómo clonar profundamente el estado de forma segura.
  • Cómo evolucionó la ingeniería frontend de lo estilizado a sistemas de scaling — Explora el cambio desde HTML/CSS/JS básicos hasta arquitecturas de componentes, caché, monorepos y capacidades de observabilidad necesarias para atender de manera fiable a millones de usuarios.