Mejorando el Rendimiento del Sitio Web Distribuyendo Más CSS
Existe un instinto arraigado en el trabajo frontend moderno que dice que la página más rápida distribuye lo mínimo de todo: menos JavaScript, menos fuentes, menos solicitudes. Por eso, cuando el equipo de ingeniería de GitHub escribió sobre la mejora del rendimiento del sitio web distribuyendo más CSS, parecía una contradicción. La solución para una interfaz lenta y cargada de JavaScript no era eliminar los estilos de la aplicación. Era volver a incorporarlos, como hojas de estilo simples, estáticas y almacenables en caché, y eliminar el JavaScript que existía únicamente para generar estilos en el navegador.
La distinción es el artículo en sí. Distribuir "más CSS" no significa una hoja de estilo más grande que hinche tu ruta crítica. Significa sacar la generación de estilos del tiempo de ejecución y llevarla a la compilación, para luego dejar que el navegador haga lo que realmente se le da bien: analizar una hoja de estilo una vez, almacenarla en caché y reutilizarla en cada navegación.
El Coste Oculto del Estilizado en Tiempo de Ejecución
Las bibliotecas CSS-en-JS resolvieron problemas reales. Dieron a los autores de componentes alcance, colocación y la capacidad de interpolar propiedades en los estilos sin inventar una convención de nomenclatura. La compensación fue arquitectónica: trasladaron un problema de tiempo de compilación al navegador.
La generación de estilos es trabajo de JavaScript
Cada página renderizada por el servidor que utiliza una biblioteca CSS-en-JS en tiempo de ejecución le pide al navegador que haga el mismo trabajo dos veces. El servidor serializa los estilos en etiquetas <style>, luego la biblioteca del cliente se activa, recorre el árbol de componentes, vuelve a generar hashes de los nombres de clase y reconcilia el resultado. Ese trabajo ocurre en el hilo principal, compitiendo con los mismos pasos de maquetación y pintado que estás tratando de acelerar.
// Runtime CSS-in-JS: this stylesheet does not exist until hydration runs
const Button = styled.button`
background: ${(p) => (p.primary ? "#1f883d" : "transparent")};
border: 1px solid #d0d7de;
padding: 6px 16px;
border-radius: 6px;
font: inherit;
`;
El mismo componente expresado como una hoja de estilo en tiempo de compilación elimina tanto la dependencia de tiempo de ejecución como el trabajo de serialización:
/* button.module.css, parsed once, cached forever */
.button {
padding: 6px 16px;
border: 1px solid #d0d7de;
border-radius: 6px;
font: inherit;
background: transparent;
}
.buttonPrimary {
background: #1f883d;
color: #ffffff;
}
import styles from "./button.module.css";
export function Button({ primary, ...props }) {
const className = primary
? `${styles.button} ${styles.buttonPrimary}`
: styles.button;
return <button className={className} {...props} />;
}
Nada de esto es novedoso. Lo que es fácil de subestimar es la cantidad de un bundle que un stack maduro de CSS-en-JS posee silenciosamente: la propia biblioteca, las utilidades de interpolación de propiedades, el proveedor de temas, el serializador y el código duplicado del framework que crece cuando cada paquete depende de su propia copia.
Se sitúa directamente en la ruta de renderizado crítica
Una etiqueta <script> bloquea el renderizado, pero lo bloquea una vez, y el navegador puede analizar el archivo mientras el flujo HTML aún está llegando. El CSS en tiempo de ejecución funciona de manera diferente: el navegador ni siquiera puede comenzar a construir los estilos del componente hasta que el JavaScript se haya descargado, analizado, compilado y ejecutado. En un teléfono de gama media, eso es un segundo o más de trabajo en el hilo principal antes de que un solo botón obtenga su color de fondo.
Fragmenta tu caché
Los nombres de clase generados cambian cada vez que cambia el código fuente del componente, lo que significa que la hoja de estilo se invalida en casi cada despliegue. Divídelo entre docenas de bundles a nivel de ruta y obtendrás una tasa de aciertos de caché que se ve saludable en un panel de control pero rara vez se activa en visitas repetidas. Una hoja de estilo estática con hash de contenido, por el contrario, se puede servir con Cache-Control: immutable y, efectivamente, nunca se descargará dos veces.
Duplica el trabajo durante la hidratación
Los desajustes de hidratación entre los estilos inyectados por el servidor y los generados por el cliente son una fuente clásica de parpadeo. El navegador pinta un conjunto de reglas, luego la biblioteca del cliente las reemplaza en medio de la interacción, forzando un recálculo de estilos y, a veces, un cambio de diseño. Los nombres de clase estáticos calculados en tiempo de compilación son idénticos en ambos lados, por lo que el primer pintado es también el pintado final.

Lo que GitHub Cambió Realmente
La migración se entiende mejor como cinco movimientos arquitectónicos en lugar de una sola refactorización. Ninguno de ellos requiere abandonar un modelo de componentes, y ninguno requiere una reescritura completa.

Los tokens de diseño se convirtieron en propiedades personalizadas de CSS
Las escalas de color, espaciado, radio y tipografía se trasladaron a un bloque global :root como propiedades personalizadas. Los componentes hacen referencia a var(--nombre-del-token) en lugar de importar un objeto de tema de JavaScript. Esto es lo que hace que el CSS estático sea lo suficientemente expresivo como para reemplazar los estilos dinámicos en la mayoría de los casos, y permite la aplicación de temas en tiempo de ejecución (modo oscuro, alto contraste) con cero JavaScript.
Estilos de componentes compilados por adelantado
Cada componente obtuvo una hoja de estilo real, emitida en tiempo de compilación y con alcance definido por un hash o una convención de módulo. El resultado es CSS ordinario que cualquier navegador puede analizar a la velocidad de análisis.
CSS crítico insertado en línea en la cabecera del documento
Solo las reglas necesarias para el primer viewport se insertan en línea en el HTML. Eso suele ser de 8 a 14 KB comprimidos con gzip, y significa que el primer pintado nunca espera un viaje de ida y vuelta de red para la hoja de estilo que da forma a la página visible.
<head>
<!-- Critical: exactly what the first viewport needs, nothing more -->
<style>
/* reset + tokens + header + hero + above-the-fold grid */
</style>
<!-- Everything else: one hashed, immutable, long-cached file -->
<link
rel="stylesheet"
href="/assets/app.8f3a1c2d.css"
media="print"
onload="this.media='all'"
/>
<noscript>
<link rel="stylesheet" href="/assets/app.8f3a1c2d.css" />
</noscript>
</head>
El marcado renderizado por el servidor lleva los nombres de clase finales
Debido a que los nombres de clase existen en tiempo de compilación, el HTML que devuelve el servidor ya está estilizado. No hay una segunda pasada, ni un paso de reconciliación, ni un destello de contenido sin estilo en conexiones lentas.
Las capas de cascada reemplazaron los trucos de especificidad
Un sistema de diseño compartido vive o muere por un orden de cascada predecible. Declarar el orden de las capas una vez, desde el principio, hace que las sobrescrituras sean deterministas y elimina el incentivo de recurrir a !important o selectores cada vez más profundos.
@layer reset, tokens, base, components, utilities;
@layer components {
.card {
background: var(--color-canvas-default);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-2);
padding: var(--space-4);
}
}
@layer utilities {
.mt-4 { margin-top: var(--space-4); }
}
Los Números: Qué Cambia Cuando Distribuyes Más CSS
Mover la generación de estilos al tiempo de compilación cambia la forma de lo que distribuyes, no solo el volumen. Los bytes que quedan son baratos: se comprimen bien, se comparten en cada ruta y se almacenan en caché durante un año.

La recompensa arquitectónica se manifiesta en cuatro lugares a la vez: menos JavaScript para descargar, menos trabajo en el hilo principal antes del primer pintado, una hoja de estilo almacenable en caché compartida entre navegaciones y un marcado idéntico en el servidor y el cliente.
| Approach | JS shipped (gzip) | CSS shipped (gzip) | Runtime style work | Cacheable across deploys |
|---|---|---|---|---|
| Runtime CSS-in-JS | ~180 KB | ~12 KB | High | No |
| Single monolithic stylesheet | ~96 KB | ~88 KB | None | Yes |
| Inline critical + split static sheets | ~78 KB | ~54 KB | None | Yes |
| Utility-first CSS | ~85 KB | ~40 KB | None | Yes |
La hoja de estilo más rápida es la que el navegador analiza una vez, almacena en caché durante un año y nunca tiene que volver a generar.
Un Manual Práctico de Migración
No necesitas la base de código de GitHub para aplicar el mismo razonamiento. La migración es incremental y cada paso se puede distribuir de forma independiente.

1. Inventario de lo que realmente se renderiza en tiempo de ejecución
Abre DevTools, carga una página y cuenta las etiquetas <style>. Si encuentras docenas de etiquetas <style> inyectadas con nombres de clase con hash, has encontrado tu objetivo. Cruza los datos con el panel de Rendimiento: busca tareas largas entre TTFB y el primer pintado de contenido.
2. Extrae el CSS de la parte superior (above the fold)
Comienza con el bloque en línea más pequeño posible. Una heurística fiable es insertar en línea el reset, los tokens y las reglas utilizadas por las alturas de los dos primeros viewports. Todo lo que está debajo del pliegue se carga desde la hoja de estilo diferida.
3. Convierte los componentes de tiempo de ejecución a clases estáticas
Trabaja componente por componente, comenzando por los de mayor tráfico. Las variantes impulsadas por propiedades se convierten en clases modificadoras o propiedades personalizadas establecidas en un contenedor. Mantén la API del componente idéntica para que los puntos de llamada no cambien.
// Genuinely dynamic values still belong in an inline style, not a generated sheet
<div
className={styles.progressBar}
style={{ "--progress": `${percent}%` }}
role="progressbar"
aria-valuenow={percent}
/>
.progressBar {
width: var(--progress, 0%);
transition: width 200ms ease-out;
}
Este patrón es el caballo de batalla de toda la migración: la estructura del estilo es estática y almacenable en caché, mientras que el valor permanece dinámico y no cuesta nada actualizarlo.
4. Establece el orden de las capas al principio
Declara el orden @layer al principio de tu hoja de estilo de entrada antes de tener un problema de especificidad, no después. Adaptar capas a una cascada heredada es mucho más doloroso que empezar con ellas.
5. Genera hashes de los nombres de archivo y almacena en caché de forma inmutableEl hash de contenido más immutable es lo que convierte "más CSS" en "CSS gratuito". La segunda vista de página en una sesión debería alcanzar la caché HTTP para cada regla de estilo del sitio.
6. Mantén lo dinámicamente estilizado como dinámico
Los valores de transformación, posición de desplazamiento y los basados en mediciones pertenecen a los estilos en línea, requestAnimationFrame o la API de Web Animations. No reintroduzcas un motor de estilos en tiempo de ejecución para resolver un problema que las propiedades personalizadas de CSS ya resuelven.
Cómo Medir la Mejora
Mide las mismas tres cosas antes y después: tiempo de pintado, tareas largas del hilo principal y recuento de hojas de estilo. Un pequeño script de PerformanceObserver te da las tres sin añadir una dependencia de monitoreo.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, Math.round(entry.startTime));
}
}).observe({ type: "paint", buffered: true });
new PerformanceObserver((list) => {
const long = list.getEntries().filter((e) => e.duration > 50);
console.log(`${long.length} long tasks after load`);
}).observe({ type: "longtask", buffered: true });
console.log("stylesheets:", document.styleSheets.length);
Espera el mayor movimiento en el tiempo hasta el primer pintado en visitas repetidas y en perfiles móviles limitados, donde domina el costo de análisis de JavaScript eliminado. El pintado de contenido más grande tiende a mejorar más gradualmente, versión tras versión.

Errores que Vale la Pena Evitar
El enfoque no está exento de riesgos, y la mayoría de los fallos provienen de aplicarlo de forma demasiado agresiva.

- **CSS crítico incompleto.**Si te equivocas en el primer viewport, obtienes un reflujo visible cuando llega la hoja diferida. Genera el bloque crítico automáticamente a partir de un renderizado real, no a mano. -**Incluir todo en línea.**Incluir la hoja de estilo completa en cada respuesta HTML anula la caché e infla el tamaño del documento. Incluye en línea el mínimo, difiere el resto. -**Instintos de empaquetarlo todo.**El viejo consejo de concatenar todo el CSS en un archivo gigante es anterior a HTTP/2. Dividir por ruta suele ser mejor ahora, siempre que las primitivas compartidas se almacenen en caché por separado. -Mezclar dos sistemas. Ejecutar CSS-in-JS en tiempo de ejecución y hojas de estilo estáticas simultáneamente durante demasiado tiempo duplica tu superficie de estilo y hace que la depuración de la cascada sea miserable. Establece una fecha límite de migración por directorio de componentes.
El error no técnico más común es tratar esto como una reescritura masiva. Los equipos que tienen éxito convierten un puñado de componentes de alto tráfico, miden el delta y usan esos datos para justificar el siguiente lote.
Usar DivMagic para Hacer Ingeniería Inversa de Arquitectura CSS Real
Una parte infravalorada de esta migración es ver cómo los sitios de producción maduros estructuran realmente sus hojas de estilo. Cuando inspeccionas un sitio que sirve bien CSS estático, puedes aprender más en cinco minutos que en una hora leyendo documentación: cómo organizan la cascada, cómo delimitan los tokens, cómo dividen lo crítico de lo diferido y cómo nombran los modificadores.
DivMagic está diseñado exactamente para ese flujo de trabajo. En lugar de copiar manualmente los estilos de los elementos de DevTools una regla a la vez, seleccionas cualquier UI en cualquier página y extraes el HTML y CSS resultantes directamente en tu editor, listos para adaptar. Esto es genuinamente útil en dos situaciones: prototipar un componente a partir de un diseño de referencia y estudiar cómo un sitio de alto rendimiento que admiras organiza su cascada para poder reflejar el patrón en tu propio sistema de diseño.
También acorta la parte aburrida del paso tres anterior. Cuando conviertes un componente de tiempo de ejecución en clases estáticas, tener los estilos calculados finales en una forma editable, en lugar de estilos en línea dispersos y bloques <style> inyectados, elimina la mayor parte del trabajo de transcripción.
Conclusiones Clave
- La generación de estilos en tiempo de ejecución es trabajo de JavaScript en el hilo principal que ocurre después de la descarga de JavaScript, directamente en tu ruta de renderizado crítica.
- El CSS estático se analiza una vez, se almacena en caché durante un año cuando tiene hash de contenido y se comparte en cada ruta y cada navegación.
- Incluye en línea solo las reglas que necesita el primer viewport; difiere el resto detrás de un
<link>no bloqueante y una política de caché larga. - Las propiedades personalizadas de CSS más
@layercubren la gran mayoría de los casos que solían requerir interpolación de estilos basada en propiedades. - Migra de forma incremental, componente por componente, y mide el tiempo de pintado p75 y las tareas largas en lugar de los promedios.
Enviar más CSS suena a regresión hasta que te das cuenta de que estás intercambiando una hoja de estilo en caché y analizada una vez por megabytes de JavaScript que la reconstruían en cada carga de página.
La lección se generaliza mucho más allá de GitHub. Cada abstracción que se ejecuta en el navegador es un costo que paga el usuario, en su dispositivo, en su red, cada vez. Mover la generación de estilos al tiempo de compilación es uno de los intercambios de mayor apalancamiento disponibles para un frontend basado en componentes: envías un archivo estático que el navegador ya sabe cómo manejar y eliminas la maquinaria que solía interponerse en su camino.
