Auditar un proyecto: el gráfico de especificidad
Cómo medir la salud de una hoja de estilos representando la especificidad frente al orden del código, qué significa cada patología del dibujo y cómo cambia la lectura cuando hay capas.
La especificidad de una regla aislada no dice nada; la distribución de especificidades de un proyecto entero lo dice todo. Representarla frente al orden del código produce un dibujo cuya forma diagnostica en treinta segundos el estado de un codebase que tardarías semanas en leer. Es la única métrica de CSS que ha resistido el paso del tiempo, y con la llegada de las capas su interpretación cambia lo justo para seguir siendo útil.
- Construir el gráfico de especificidad de un proyecto a partir del navegador.
- Interpretar las tres patologías clásicas del dibujo y su causa habitual.
- Elegir métricas complementarias que resuman el estado de una hoja de estilos.
- Reinterpretar el gráfico en un proyecto que usa capas en cascada.
Qué forma tiene un gráfico sano
El gráfico es un dibujo elemental: en el eje horizontal, el orden de las reglas tal y como se aplican; en el vertical, la especificidad de cada una. La idea, que se popularizó a mediados de la década pasada, es que la forma resultante revela si el código coopera con la cascada o pelea contra ella.
Un gráfico sano tiende a ascender suavemente. La razón es que si el CSS está ordenado de lo general a lo particular —primero el reset, luego los elementos, luego los componentes, luego las variaciones—, cada bloque necesita ganarle al anterior y lo consigue por orden, sin necesidad de subir la especificidad. La pendiente suave refleja que la especificidad crece porque los selectores acotan más, no porque estén compitiendo.
Un gráfico enfermo tiene picos. Cada pico es una regla que necesitó ser mucho más específica que sus vecinas, y detrás de cada pico hay siempre la misma historia: alguien perdió una pelea y escaló. El pico no es el problema; es la cicatriz de un problema que ocurrió antes, normalmente varios cientos de líneas más arriba.
Hay una variante particularmente mala: el pico temprano. Una regla muy específica al principio del fichero obliga a que todo lo posterior que quiera tocar esos elementos supere esa cota, con lo que la especificidad no vuelve a bajar nunca. Es el efecto trinquete de la lección anterior, visto como dibujo.
Sacarlo de un proyecto real
No hace falta una herramienta dedicada. Todo el material está en el modelo de objetos de CSS y se recoge desde la consola del navegador. Este fragmento aplana las reglas anidadas dentro de @media, @supports y @layer, y devuelve las de estilo en el orden en que el navegador las tiene:
const aplanar = (reglas) =>
[...reglas].flatMap((r) => (r.cssRules ? aplanar(r.cssRules) : [r]));
const todas = [...document.styleSheets].flatMap((hoja) => {
try { return aplanar(hoja.cssRules); } catch { return []; }
});
const deEstilo = todas.filter((r) => r.selectorText);
console.log('reglas de estilo', deEstilo.length);
El try no es adorno: leer las reglas de una hoja servida desde otro dominio lanza una excepción de seguridad, y sin él la auditoría se interrumpe en la primera fuente externa.
Sobre esa lista se construye el informe. Este cuenta identificadores, clases y declaraciones importantes por regla, que son los tres indicadores que localizan el noventa por ciento de los puntos calientes:
const informe = deEstilo.map((r, i) => ({
orden: i,
selector: r.selectorText,
ids: (r.selectorText.match(/#[\w-]+/g) || []).length,
clases: (r.selectorText.match(/\.[\w-]+/g) || []).length,
importantes: [...r.style].filter(
(p) => r.style.getPropertyPriority(p) === 'important'
).length,
}));
console.table(
informe.filter((x) => x.ids > 0 || x.importantes > 0 || x.clases > 2)
);
console.log({
total: informe.length,
conId: informe.filter((x) => x.ids > 0).length,
conImportante: informe.filter((x) => x.importantes > 0).length,
});
Conviene ser honesto sobre lo que es esto: una heurística de auditoría, no una calculadora de especificidad. Cuenta símbolos, así que no aplica la regla del argumento más específico dentro de :is() ni descuenta lo que hay dentro de :where(). Para lo que sirve —encontrar dónde mirar— es más que suficiente, y tiene la ventaja de correr sobre el CSS realmente cargado, que en un proyecto con construcción y dependencias no siempre coincide con lo que hay en tu repositorio.
Las tres patologías
Sierra. Sube y baja constantemente. Indica que hay dos o más grupos de reglas compitiendo por los mismos elementos, típicamente porque el CSS está organizado por página o por pantalla en lugar de por componente. Cada pantalla vuelve a estilar lo mismo con un selector algo más específico que la anterior.
Meseta alta. Todo el gráfico está por encima de un umbral, sin picos. Es el trinquete consumado: alguien puso un identificador o un prefijo global —el clásico body.tema-x delante de todo— y a partir de ahí escribir por debajo dejó de funcionar. No hay peleas visibles porque la guerra terminó; el suelo simplemente subió.
Muro final. Un bloque de reglas al final con especificidad alta o con importancia. Suele ser el fichero de sobrescrituras, ese que se llama overrides, custom o fixes. Es la capitulación explícita: alguien decidió que era más barato ganar por orden y por fuerza bruta que entender el sistema. Funciona hasta que hay dos ficheros así.
Conviene entender qué estaba midiendo realmente este gráfico, porque su interpretación clásica ha quedado a medias. Lo que un gráfico ascendente indicaba era que el proyecto había conseguido codificar su jerarquía de autoridad en la combinación de orden y especificidad, que eran las dos únicas herramientas disponibles. La pendiente suave era el síntoma de que el orden del código coincidía con el orden de autoridad deseado, y los picos eran el síntoma de que en algún punto esa coincidencia se rompió y hubo que compensar con especificidad. Con @layer la autoridad se declara aparte, así que la métrica correcta ya no es la pendiente global sino otra: la especificidad debería ser plana dentro de cada capa. Una capa sana tiene todas sus reglas en torno a 0-1-0, porque dentro de una capa nadie necesita ganarle a nadie por especificidad: quien tenía que perder ya pierde por estar en una capa anterior. Un pico dentro de una capa sigue significando exactamente lo mismo que antes, una pelea, pero ahora es una pelea entre iguales y casi siempre revela que ese bloque debería estar dividido en dos capas. Hay además una advertencia que invalida el gráfico clásico en aplicaciones modernas y que no se suele mencionar: si tu CSS se carga en fragmentos según la navegación, el eje horizontal no es determinista, porque el orden de aparición depende de por dónde entró el usuario. En ese escenario un gráfico ascendente no demuestra nada, porque el orden que mediste es solo uno de los posibles. La única forma de recuperar una métrica con sentido es declarar el orden de capas explícitamente y medir dentro de cada capa; ahí sí el dibujo vuelve a decir la verdad.
Qué medir además del dibujo
Cuatro cifras resumen el estado de una hoja de estilos mejor que cualquier impresión subjetiva, y las cuatro salen del informe anterior.
Número de selectores con identificador. Idealmente cero. Cualquier valor distinto de cero merece una lista nominal, porque cada uno impone un suelo.
Número de declaraciones importantes, separadas por si están o no dentro de una capa. Las que estén dentro de una capa de utilidades declarada la primera son deliberadas; las demás son deuda hasta que se demuestre lo contrario.
Especificidad mediana. Es más informativa que la máxima. Una mediana en 0-1-0 describe un sistema plano y sano; una mediana en 0-3-0 describe un sistema donde acotar se ha vuelto la norma y por tanto sobrescribir es caro para todos.
Proporción de reglas por encima de la mediana. Si es pequeña, tienes puntos calientes localizados y el arreglo es quirúrgico. Si es grande, el problema es estructural y el arreglo es de capas, no de selectores.
Esa última distinción es la que decide la estrategia de saneamiento, y es el asunto de la última lección del nivel: cuándo merece la pena bajar selectores uno a uno y cuándo lo único sensato es dejar de competir.