wandres.dev
RENDIMIENTO DEL CSS · contain, content-visibility y el coste

Los mitos del rendimiento de CSS, con datos

Por qué los selectores descendentes dejaron de importar hace una década: emparejado de derecha a izquierda, filtros de Bloom, conjuntos de invalidación, y lo que sí queda de verdad.

⏱ 18 min

El consejo de rendimiento de CSS que más se repite —evita los selectores descendentes, no anides, pon la clase más específica a la derecha— procede de mediciones hechas alrededor de 2009 sobre motores que ya no existen, y su propio autor original señaló entonces que el efecto era despreciable en páginas reales. Quince años después se sigue repitiendo mientras el coste real está en otro sitio, y el resultado es equipos que optimizan selectores durante una tarde y no tocan lo que de verdad les cuesta cien veces más.

🎯 Al terminar esta lección sabrás
  • Explicar cómo empareja selectores un motor moderno y por qué el orden importa.
  • Describir las tres optimizaciones que hacen barato el emparejado hoy.
  • Situar el coste del emparejado frente al del layout y el paint con órdenes de magnitud.
  • Identificar los pocos casos en que un selector sí puede costar caro.

El emparejado va de derecha a izquierda

La primera intuición de casi todo el mundo es falsa. Al leer .barra ul li a, uno imagina al motor buscando las barras, luego las listas dentro, luego los elementos, luego los enlaces. Si funcionara así, un selector largo sería catastrófico.

No funciona así. El motor empareja de derecha a izquierda, empezando por lo que se llama el selector clave —la parte más a la derecha— y subiendo por el árbol solo si esa parte coincide. El proceso real para pintar un elemento es el inverso: para cada elemento, busca qué reglas le podrían aplicar.

La consecuencia es que la longitud del selector casi no importa, porque en la inmensa mayoría de los casos el motor descarta la regla en el primer paso. Un a que no está dentro de un li descarta .barra ul li a tras comprobar un solo antepasado. Lo que importa no es cuántas partes tiene el selector, sino cuántos elementos coinciden con su parte más a la derecha.

De ahí sale la única regla de escritura de selectores que tiene fundamento, y no es la que se repite: el selector clave debe ser selectivo. .barra * es peor que .barra a porque el asterisco coincide con todo y obliga a subir por el árbol desde cada elemento del documento.

Las tres optimizaciones que lo hacen barato

Los motores modernos no comparan cada elemento contra cada regla. Hacen tres cosas que reducen el trabajo en varios órdenes de magnitud.

Reparto por selector clave. Al parsear la hoja, cada regla se guarda en un cubo según su selector clave: uno para las que terminan en una clase concreta, otro para las que terminan en un identificador, otro por etiqueta. Al estilar un elemento, el motor solo mira los cubos que le corresponden. Un elemento div con dos clases consulta tres cubos pequeños, no las diez mil reglas de la hoja.

Filtros de Bloom para los antepasados. Al recorrer el árbol, el motor mantiene una estructura probabilística compacta con las clases, identificadores y etiquetas de todos los antepasados del elemento actual. Antes de subir por el árbol para verificar .barra ul li a, consulta el filtro: si .barra no está en el conjunto de antepasados, descarta la regla sin tocar el árbol. El filtro puede dar falsos positivos y nunca falsos negativos, que es exactamente la garantía que hace falta: cuando dice que no, es que no, y ese es el caso mayoritario.

Conjuntos de invalidación. Es la optimización más importante y la peor conocida. Cuando cambias una clase en un elemento, un motor ingenuo tendría que recalcular el estilo de ese elemento y de todo su subárbol. En su lugar, al parsear la hoja el motor construye un índice inverso: para cada clase, qué propiedades y qué elementos podrían verse afectados si esa clase aparece o desaparece. Al cambiar la clase, solo se recalcula lo que ese índice señala. Añadir .activo a un botón, si .activo solo aparece en reglas que afectan al propio elemento, invalida un elemento, no un subárbol.

ℹ️
El dato original, y lo que decía de verdad

Las mediciones que originaron el consejo clásico eran sintéticas: páginas construidas con miles de reglas y miles de elementos para amplificar el efecto. Incluso allí, la diferencia entre el peor selector y el mejor se contaba en decenas de milisegundos sobre el total de una carga, y el propio análisis concluía que en páginas reales el impacto era despreciable frente al resto del trabajo. El consejo se propagó; la conclusión, no.

Los órdenes de magnitud

Para calibrar, conviene tener presentes las magnitudes relativas del trabajo que hace un navegador en una interacción típica. No son cifras exactas de ningún dispositivo concreto, son proporciones que se repiten en casi cualquier perfil real:

Trabajo Coste relativo
Emparejar selectores de un elemento despreciable
Recalcular estilo de un subárbol grande moderado
Layout de un subárbol grande alto
Layout síncrono forzado, repetido en un bucle catastrófico
Pintar un área grande con desenfoque alto
Pintar un área pequeña bajo
Componer capas ya pintadas muy bajo

Lo que hay que leer en esa tabla es que la primera fila y la cuarta están separadas por varios órdenes de magnitud, y que la gente pasa su tiempo optimizando la primera. Si tienes un problema de rendimiento de CSS, casi con certeza está en el layout o en el paint, y el siguiente capítulo es sobre eso.

Lo que sí puede costar caro

Ser justos exige reconocer los casos reales, que existen y son pocos.

:has() con sujetos amplios. Es el único selector moderno con un coste estructural distinto, porque invalida hacia arriba: cuando algo cambia dentro de un subárbol, el motor tiene que reconsiderar si algún antepasado dejó de coincidir con un :has(). Los motores lo optimizan bastante, pero :has() sobre selectores muy generales —o sobre elementos que cambian con mucha frecuencia— sí aparece en los perfiles. La mitigación es acotar el sujeto: .tarjeta:has(> img) en lugar de :has(img).

Hojas enormes con muchísimas reglas y contenido muy dinámico. El coste de construir los índices se paga una vez, pero las hojas de cientos de miles de reglas hacen crecer el trabajo de recálculo cuando el DOM cambia constantemente. Es un problema de tamaño total, no de forma de los selectores.

El propio hecho de que el CSS bloquee el renderizado. Esta es la que sí importa de verdad y no tiene nada que ver con los selectores: el navegador no pinta nada hasta que ha descargado y parseado el CSS que aplica al documento. Cien kilobytes de CSS en el camino crítico retrasan el primer pintado en una red lenta muchísimo más de lo que cualquier selector puede costar en toda la vida de la página. Y las cadenas de @import son peores, porque el navegador no descubre el segundo fichero hasta que ha parseado el primero: son descargas en serie donde deberían ser en paralelo.

Las animaciones que tocan propiedades de layout. Animar width o top obliga a rehacer el layout en cada fotograma. Eso no es un problema de selectores: es elegir mal la propiedad, y está cubierto por lo que vimos sobre transformaciones y el pipeline de renderizado.

Los consejos de rendimiento envejecen mal porque describen implementaciones, no principios

Merece la pena entender por qué este mito concreto ha sobrevivido quince años, porque el mecanismo se repite con todos los demás. Un consejo de rendimiento nace de una medición sobre una implementación concreta en un momento concreto. Mientras esa implementación es la dominante, el consejo es verdad. Cuando la implementación cambia —y cambia constantemente, porque los motores compiten precisamente en eso— el consejo se queda huérfano de su justificación, pero no de su forma: sigue siendo una frase corta, fácil de repetir y aparentemente prudente, y esas tres propiedades bastan para que se propague sin que nadie vuelva a medir. Encima tiene un mecanismo de refuerzo perverso: como seguirlo no hace daño visible, nadie descubre que es inútil. Cuesta tiempo y no ahorra nada, pero el coste es invisible y el supuesto beneficio es incomprobable. La defensa práctica no es memorizar qué consejos han caducado —esta lista volverá a caducar— sino cambiar la pregunta que te haces. En lugar de “¿es esto rápido?”, pregunta “¿qué trabajo hace el navegador por mi culpa, y con qué frecuencia lo repite?”. Esa pregunta sobrevive a cualquier cambio de implementación, porque el trabajo que hay que hacer —resolver estilos, calcular geometría, rasterizar píxeles, componer— es una propiedad del problema y no del motor. Y tiene una ventaja adicional decisiva: se puede responder con un perfilador en cinco minutos, mientras que “¿es esto rápido?” solo se puede responder con opiniones.