
Durante mucho tiempo, styled-components fue mi elección predeterminada para construir Design Systems. Resolvía el problema del scoping, hacía que los estilos dinámicos basados en props fueran intuitivos y se sentía como "simplemente escribir JavaScript".
Pero el panorama ha cambiado. Con el auge de los React Server Components (RSC) y el empuje constante por mejores Core Web Vitals, las desventajas del CSS-in-JS en tiempo de ejecución son cada vez más difíciles de justificar.
Recientemente, realicé un análisis profundo para seleccionar el próximo motor de estilos para la migración de una plataforma B2B que estoy diseñando. Comparé nuestra configuración actual contra contendientes modernos como Tailwind CSS, CSS Modules y Zero-Runtime CSS-in-JS (específicamente Vanilla Extract).
Aquí está mi análisis, los datos detrás de la decisión y hacia qué me estoy moviendo.
Table of Contents
- El problema con CSS-in-JS en tiempo de ejecución
- Los contendientes: Mi evaluación
- La comparación de código
- Regla de decisión: ¿Cuál deberías elegir?
- Pensamientos finales
- References
El problema con CSS-in-JS en tiempo de ejecución
Para entender por qué nos estamos moviendo, tenemos que mirar cómo funciona styled-components. Cuando tu aplicación carga, la librería parsea tus definiciones de estilos, genera clases CSS al vuelo y las inyecta en el <head>.
Esto introduce dos cuellos de botella significativos:
- Sobrecarga en tiempo de ejecución: El navegador tiene que hacer trabajo extra de JavaScript (parsing/inyección) antes de que pueda pintar la pantalla. En dispositivos de gama baja o dashboards grandes, esto añade latencia medible al First Contentful Paint (FCP).
- La fricción con Server Components: En el mundo del App Router de Next.js, styled-components te fuerza a marcar partes significativas de tu árbol con
"use client". Esto niega muchos beneficios de los RSCs, ya que no puedes renderizar componentes estilados puramente en el servidor sin costes de hidratación.
Como he escrito antes, las arquitecturas modernas nos requieren pensar sobre dónde se ejecuta nuestro código: en el servidor o en el cliente. Las librerías de estilos en tiempo de ejecución desdibujan esa línea de una manera que se está volviendo cada vez más pesada.
Los contendientes: Mi evaluación
Evalué tres caminos principales hacia adelante. Aquí está el resumen de mis hallazgos.
1. El gigante Utility-First: Tailwind CSS
Tailwind ha ganado el concurso de popularidad, y por una buena razón. Es atómico, escanea tus archivos en tiempo de compilación y genera un archivo CSS minúsculo que contiene solo lo que usas.
- Pros: Costo cero en tiempo de ejecución. Excelente colinealidad (los estilos viven con el marcado). La velocidad de copiar y pegar es inigualable.
- Cons: La "sopa de clases" puede hacer que los componentes complejos sean difíciles de leer. Enforzar tokens de un sistema de diseño estricto requiere disciplina (o herramientas extra).
Veredicto: Increíble para productos, pero a veces carece de esa sensación de "API" estricta que quiero para un Design System rigurosamente restringido.
2. El tradicionalista: CSS Modules
Esta es la elección estándar "aburrida". Ámbito local para estilos por defecto y requiere cero librerías en tiempo de ejecución.
- Pros: Soporte nativo de CSS. Sin bloqueo (lock-in). Funciona perfectamente con Server Components.
- Cons: Pierdes el estyling dinámico "basado en props". Tienes que mapear manualmente props a nombres de clases, resultando a menudo en lógica de
classNameverbosa.
Veredicto: Fiable, pero se siente como un paso atrás en Developer Experience (DX) comparado con el modelo basado en componentes al que estamos acostumbrados.
3. El híbrido moderno: Zero-Runtime CSS-in-JS (Vanilla Extract)
Esta categoría promete lo mejor de ambos mundos: escribes código que parece objetos TypeScript, pero se compila a clases CSS estáticas en tiempo de construcción.
- Pros: Estilos 100% tipados. Costo cero en tiempo de ejecución. Gran compatibilidad con RSCs ya que genera archivos
.cssestáticos. - Cons: Requiere un plugin de bundler. Generalmente necesitas archivos
.css.tsseparados, lo cual rompe ligeramente el modelo mental de "single file component".
Veredicto: El "Sueño del Arquitecto". Ofrece restricciones estrictas y seguridad de tipos sin la penalización de rendimiento.
La comparación de código
Miremos un ejemplo concreto: un componente Button con variantes primaria y secundaria.
La vieja forma: Styled-Components
Esto depende de la evaluación de props en tiempo de ejecución.
import styled from 'styled-components';
const Button = styled.button<{ $variant: 'primary' | 'secondary' }>`
padding: 12px 24px;
border-radius: 8px;
border: none;
background-color: ${props => props.$variant === 'primary' ? '#0070f3' : '#eaeaea'};
color: ${props => props.$variant === 'primary' ? 'white' : 'black'};
&:hover {
opacity: 0.9;
}
`;
La nueva forma: Tailwind + CVA (Class Variance Authority)
Este es el patrón que he estado adoptando más frecuentemente. Usamos CVA para traer de vuelta la "interfaz" de un componente mientras usamos Tailwind para el motor.
import { cva, type VariantProps } from 'class-variance-authority';
// 1. Define the variants
const buttonVariants = cva(
// Base styles
"py-3 px-6 rounded-lg border-none transition-opacity hover:opacity-90 font-medium",
{
variants: {
intent: {
primary: "bg-blue-600 text-white",
secondary: "bg-gray-200 text-black",
},
},
defaultVariants: {
intent: "primary",
},
}
);
// 2. Create the component
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button = ({ intent, className, ...props }: ButtonProps) => {
return (
<button
className={buttonVariants({ intent, className })}
{...props}
/>
);
};
Este enfoque me permite mantener la API que mi equipo ama (<Button intent="primary" />) pero elimina completamente el costo de estilo en tiempo de ejecución.
Regla de decisión: ¿Cuál deberías elegir?
Después de migrar varios sistemas de grado empresarial, aquí está la heurística que uso:
- Usa Tailwind + CVA si: Estás construyendo una aplicación enfocada en el producto o una herramienta interna flexible donde la velocidad de iteración es la prioridad máxima. El soporte del ecosistema (shadcn/ui, etc.) hace que este sea el defecto pragmático para el 90% de los equipos.
- Usa Vanilla Extract (Zero-Runtime) si: Estás construyendo un Design System estricto y multi-marca destinado a ser consumido por cientos de desarrolladores dispares. Si necesitas enforzar seguridad de tipos en tus tokens (ej. asegurando que los devs solo usen
vars.color.brandy nunca códigos hex), la integración con TypeScript es superior. - Quédate con Styled-Components si: Tienes una base de código legado masiva sin problemas de rendimiento, y no planeas moverte a React Server Components pronto. La migración es costosa; no lo hagas solo por el hype.
Pensamientos finales
Las decisiones tecnológicas raramente son sobre "correcto" o "incorrecto"; se trata de trade-offs. Hace cinco años, styled-components era el trade-off correcto para el aislamiento de componentes. Hoy, con la línea base del navegador subiendo y el cambio arquitectónico hacia el servidor, la extracción estática es la ganadora.
Actualmente estoy apostando por Tailwind con CVA para desarrollo de aplicaciones y explorando Vanilla Extract para desarrollo de librerías estrictas. El objetivo es siempre el mismo: una gran experiencia de desarrollador que no cobre impuestos al navegador del usuario.