Más allá de las expresiones regulares: Mejora del análisis del valor de CSS en las Herramientas para desarrolladores de Chrome

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

¿Notaste que las propiedades de CSS de la pestaña Styles de las Herramientas para desarrolladores de Chrome se ven un poco más pulidas últimamente? Estas actualizaciones, que se lanzaron entre la versión 121 y la 128 de Chrome, son el resultado de una mejora significativa en la forma en que analizamos y presentamos los valores de CSS. En este artículo, te explicaremos los detalles técnicos de esta transformación: pasar de un sistema de coincidencia de expresiones regulares a un analizador más sólido.

Comparemos las DevTools actuales con la versión anterior:

Parte superior: Es la versión más reciente de Chrome. Parte inferior: Chrome 121.

Una gran diferencia, ¿verdad? A continuación, se muestra un desglose de las mejoras clave:

  • color-mix: Es una vista previa práctica que representa visualmente los dos argumentos de color dentro de la función color-mix.
  • pink: Es una vista previa del color en la que se puede hacer clic para el color pink con nombre. Haz clic en él para abrir un selector de color y realizar ajustes fácilmente.
  • var(--undefined, [fallback value]). Se mejoró el manejo de las variables no definidas, con la variable no definida inhabilitada y el valor de resguardo activo (en este caso, un color HSL) que se muestra con una vista previa de color en la que se puede hacer clic.
  • hsl(…): Otra vista previa de color en la que se puede hacer clic para la función de color hsl, que proporciona acceso rápido al selector de color.
  • 177deg: Es un reloj en ángulo en el que se puede hacer clic y que te permite arrastrar y modificar de forma interactiva el valor del ángulo.
  • var(--saturation, …): Es un vínculo en el que se puede hacer clic a la definición de propiedad personalizada, que facilita ir a la declaración relevante.

La diferencia es sorprendente. Para lograrlo, tuvimos que enseñarle a DevTools a comprender los valores de las propiedades CSS mucho mejor que antes.

¿No estaban disponibles estas vistas previas?

Si bien estos íconos de vista previa pueden parecer familiares, no siempre se muestran de forma coherente, en especial en sintaxis CSS complejas como el ejemplo anterior. Incluso en los casos en que funcionaban, a menudo se requería un esfuerzo significativo para que funcionaran correctamente.

El motivo es que el sistema para analizar valores ha crecido de forma orgánica desde los primeros días de DevTools. Sin embargo, no ha podido seguir el ritmo de las increíbles funciones nuevas que obtenemos de CSS y el aumento correspondiente en la complejidad del lenguaje. El sistema requería un rediseño completo para seguir el ritmo de la evolución, y eso es exactamente lo que hicimos.

Cómo se procesan los valores de las propiedades CSS

En DevTools, el proceso de renderización y decoración de las declaraciones de propiedades en la pestaña Styles se divide en dos fases distintas:

  1. Análisis estructural. En esta fase inicial, se analiza la declaración de propiedad para identificar los componentes subyacentes y las relaciones. Por ejemplo, en la declaración border: 1px solid red, reconocería 1px como una longitud, solid como una cadena y red como un color.
  2. Renderización. En función del análisis estructural, la fase de renderización transforma estos componentes en una representación HTML. Esto enriquece el texto de la propiedad que se muestra con elementos interactivos y señales visuales. Por ejemplo, el valor de color red se renderiza con un ícono de color en el que se puede hacer clic que, cuando se hace clic, revela un selector de color para facilitar la modificación.

Expresiones regulares

Anteriormente, usábamos expresiones regulares (regex) para analizar los valores de las propiedades en el análisis estructural. Mantuvimos una lista de regex para que coincidan con los bits de valores de propiedad que consideramos decorar. Por ejemplo, había expresiones que coincidían con los colores, las longitudes y los ángulos de CSS, con subexpresiones más complicadas, como las llamadas a función var, etcétera. Analizamos el texto de izquierda a derecha para hacer un análisis de valores y buscamos de forma continua la primera expresión de la lista que coincida con la siguiente parte del texto.

Si bien esto funcionó bien la mayor parte del tiempo, la cantidad de casos en los que no lo hizo siguió creciendo. A lo largo de los años, recibimos una gran cantidad de informes de errores en los que la coincidencia no era del todo correcta. A medida que las corregimos (algunas correcciones simples, otras bastante elaboradas), tuvimos que repensar nuestro enfoque para mantener a raya nuestra deuda técnica. Veamos algunos de los problemas.

Coincide con color-mix()

La regex que usamos para la función color-mix() fue la siguiente:

/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g

Que coincide con su sintaxis:

color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})

Intenta ejecutar el siguiente ejemplo para visualizar las coincidencias.

const re = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;

// it works - simpler example
const simpler = re.exec('color-mix(in srgb, pink, hsl(127deg 100% 50%))');
console.table(simpler.groups);

re.exec('');

// it doesn't work - complex example
const complex = re.exec('color-mix(in srgb, pink, var(--undefined, hsl(127deg var(--saturation, 100%) 50%)))');
console.table(complex.groups);

Resultado de la coincidencia para la función color-mix.

El ejemplo más simple funciona bien. Sin embargo, en el ejemplo más complejo, la coincidencia <firstColor> es hsl(177deg var(--saturation y la coincidencia <secondColor> es 100%) 50%)), lo que no tiene sentido.

Sabíamos que esto era un problema. Después de todo, CSS como lenguaje formal no es normal, por lo que ya incluimos manejo especial para tratar argumentos de funciones más complicados, como las funciones var. Sin embargo, como puedes ver en la primera captura de pantalla, eso aún no funcionaba en todos los casos.

Coincidencias con tan()

Uno de los errores más divertidos informados fue sobre la función trigonométrica tan() . La regex que usábamos para hacer coincidir los colores incluía una subexpresión \b[a-zA-Z]+\b(?!-) para hacer coincidir colores con nombre, como la palabra clave red. Luego, verificamos si la parte coincidente es en realidad un color con nombre y adivina qué, tan también es un color con nombre. Por lo tanto, interpretamos erróneamente las expresiones tan() como colores.

Coincidencias con var()

Veamos otro ejemplo: funciones var() con un resguardo que contiene otras referencias de var(): var(--non-existent, var(--margin-vertical)).

Nuestra regex para var() coincidiría con este valor. Excepto, dejaría de coincidir en el primer paréntesis de cierre. Por lo tanto, el texto anterior coincide como var(--non-existent, var(--margin-vertical). Esta es una limitación de los libros de texto de la coincidencia de expresiones regulares. En esencia, los idiomas que requieren el paréntesis de coincidencia no son regulares.

Transición a un analizador de CSS

Cuando el análisis de texto con expresiones regulares deja de funcionar (porque el lenguaje analizado no es regular), hay un siguiente paso canónico: usar un analizador para una gramática de tipo superior. Para CSS, eso significa un analizador para lenguajes sin contexto. De hecho, ese sistema de analizador ya existía en la base de código de Herramientas para desarrolladores: Lezer de CodeMirror, que es la base, por ejemplo, del resaltado de sintaxis en CodeMirror, el editor que se encuentra en el panel Sources. El analizador de CSS de Lezer nos permitió producir árboles de sintaxis (no abstractos) para las reglas de CSS y estaba listo para que lo usáramos. Victoria.

Árbol de sintaxis para el valor de propiedad `hsl(177deg var(--saturation, 100%) 50%)`. Es una versión simplificada del resultado producido por el analizador Lezer, que no incluye nodos puramente sintácticos para las comas y los paréntesis.

Excepto que, desde el primer momento, nos pareció inviable migrar directamente de la coincidencia basada en regex a la basada en analizadores: los dos enfoques funcionan desde direcciones opuestas. Cuando se comparaban valores con expresiones regulares, DevTools analizaba la entrada de izquierda a derecha y, de forma reiterada, intentaba encontrar la coincidencia más antigua de una lista ordenada de patrones. Con un árbol de sintaxis, la coincidencia comenzará desde abajo hacia arriba; por ejemplo, analizando primero los argumentos de una llamada, antes de intentar hacer coincidir la llamada a función. Piensa en ello como la evaluación de una expresión aritmética, en la que primero considerarías las expresiones entre paréntesis, luego los operadores multiplicativos y, luego, los operadores aditivos. En este marco, la coincidencia basada en regex corresponde a la evaluación de la expresión aritmética de izquierda a derecha. Realmente no queríamos reescribir todo el sistema de coincidencias desde cero: había 15 pares de comparadores y procesadores diferentes, con miles de líneas de código, lo que hacía que fuera improbable que pudiéramos enviarlo en un solo evento importante.

Así que ideamos una solución que nos permitió realizar cambios incrementales, que describiremos a continuación con más detalle. En resumen, mantuvimos el enfoque de dos fases, pero en la primera fase intentamos hacer coincidir las subexpresiones de abajo hacia arriba (lo que rompe con el flujo de regex) y, en la segunda fase, renderizamos de arriba hacia abajo. En ambas fases, pudimos usar los comparadores y renderizaciones existentes basados en regex, prácticamente sin cambios, y, por lo tanto, pudimos migrarlos uno por uno.

Fase 1: Coincidencia ascendente

La primera fase hace, más o menos exactamente y de forma exclusiva, lo que dice en la portada. Atravesamos el árbol de abajo hacia arriba y tratamos de hacer coincidir las subexpresiones en cada nodo del árbol de sintaxis que visitamos. Para hacer coincidir una subexpresión específica, un comparador puede usar una regex del mismo modo que lo hizo en el sistema existente. A partir de la versión 128, todavía lo hacemos en algunos casos, por ejemplo, para coincidir con las longitudes. Como alternativa, un comparador puede analizar la estructura del subárbol con raíz en el nodo actual. Esto le permite detectar errores sintácticos y registrar la información estructural al mismo tiempo.

Considera el ejemplo del árbol de sintaxis anterior:

Fase 1: Coincidencia ascendente en el árbol de sintaxis

Para este árbol, nuestros comparadores se aplicarían en el siguiente orden:

  1. hsl(177degvar(--saturation, 100%) 50%): Primero, descubrimos el primer argumento de la llamada a función hsl, el ángulo de matiz. Lo hacemos coincidir con un comparador de ángulos para que podamos decorar el valor del ángulo con el ícono de ángulo.
  2. hsl(177degvar(--saturation, 100%)50%): En segundo lugar, descubrimos la llamada a la función var con un comparador var. Para esas llamadas, queremos hacer principalmente dos cosas:
    • Busca la declaración de la variable y calcula su valor, y agrega un vínculo y un cuadro emergente al nombre de la variable para conectarte a ellos, respectivamente.
    • Decora la llamada con un ícono de color si el valor calculado es un color. En realidad, hay un tercer punto, pero hablaremos de eso más adelante.
  3. hsl(177deg var(--saturation, 100%) 50%): Por último, hacemos coincidir la expresión de llamada de la función hsl para que podamos decorarla con el ícono de color.

Además de buscar subexpresiones que nos gustaría decorar, en realidad, hay una segunda función que ejecutamos como parte del proceso de coincidencia. Ten en cuenta que, en el paso 2, dijimos que buscamos el valor calculado de un nombre de variable. De hecho, vamos un paso más allá y propagamos los resultados hacia arriba en el árbol. Y no solo para la variable, sino también para el valor de resguardo. Se garantiza que, cuando se visita un nodo de función var, se visitaron sus elementos secundarios de antemano, por lo que ya conocemos los resultados de cualquier función var que pueda aparecer en el valor de resguardo. Por lo tanto, podemos sustituir las funciones var con sus resultados sobre la marcha de forma fácil y económica, lo que nos permite responder de forma trivial preguntas como "¿El resultado de esta llamada a var es un color?", como lo hicimos en el paso 2.

Fase 2: Renderización de arriba abajo

En la segunda fase, invertimos la dirección. Tomando los resultados de la coincidencia de la fase 1, renderizamos el árbol en HTML recorriéndolo de arriba abajo. Para cada nodo visitado, verificamos si coincide y, de ser así, llamamos al renderizador correspondiente del comparador. Evitamos la necesidad de un manejo especial para los nodos que solo contienen texto (como el NumberLiteral "50%") si incluimos un comparador y un renderizador predeterminados para los nodos de texto. Los renderizadores simplemente generan nodos HTML que, cuando se combinan, producen la representación del valor de la propiedad, incluidas sus decoraciones.

Fase 2: Renderización descendente en el árbol de sintaxis

En el árbol de ejemplo, este es el orden en el que se renderiza el valor de la propiedad:

  1. Visita la llamada a función hsl. Coincidió, así que llama a la función de renderizador de color. Hace dos cosas:
    • Calcula el valor de color real con el mecanismo de sustitución sobre la marcha para cualquier argumento var y, luego, dibuja un ícono de color.
    • Renderiza de forma recursiva los elementos secundarios de CallExpression. Esto se encarga automáticamente de renderizar el nombre de la función, los paréntesis y las comas, que son solo texto.
  2. Visita el primer argumento de la llamada a hsl. Coincidieron, así que llama al renderizador de ángulos, que dibuja el ícono de ángulo y el texto del ángulo.
  3. Visita el segundo argumento, que es la llamada a var. Coincidió, así que llama a la var renderer, que muestra lo siguiente:
    • El texto var( al comienzo.
    • El nombre de la variable y lo decora con un vínculo a la definición de la variable o con un color de texto gris para indicar que no se definió. También agrega un cuadro emergente a la variable para mostrar información sobre su valor.
    • La coma y, luego, renderiza recursivamente el valor de resguardo.
    • Un paréntesis de cierre.
  4. Visita el último argumento de la llamada a hsl. No hubo coincidencias, por lo que solo se muestra el contenido de texto.

¿Te diste cuenta de que, en este algoritmo, una renderización controla por completo cómo se renderizan los elementos secundarios de un nodo coincidente? La renderización recursiva de los elementos secundarios es proactiva. Este truco permitió una migración paso a paso de la renderización basada en regex a la renderización basada en el árbol de sintaxis. En el caso de los nodos que coinciden con un comparador de regex heredado, se puede usar el renderizador correspondiente en su forma original. En términos del árbol de sintaxis, se responsabilizaría de renderizar todo el subárbol, y su resultado (un nodo HTML) se podría conectar de forma clara al proceso de renderización circundante. Esto nos dio la opción de portar comparadores y renderizadores en pares y cambiarlos uno por uno.

Otra función genial de los procesadores que controlan la renderización de los elementos secundarios de sus nodos coincidentes es que nos da la capacidad de razonar sobre las dependencias entre los íconos que estamos agregando. En el ejemplo anterior, el color que produce la función hsl, obviamente, depende de su valor de tono. Eso significa que el color que muestra el ícono de color depende del ángulo que muestra el ícono de ángulo. Si el usuario abre el editor de ángulos a través de ese ícono y modifica el ángulo, ahora podemos actualizar el color del ícono de color en tiempo real:

Como puedes ver en el ejemplo anterior, también usamos este mecanismo para otras vinculaciones de íconos, como color-mix() y sus dos canales de color, o las funciones var que muestran un color de su resguardo.

Impacto en el rendimiento

Cuando analizamos este problema para mejorar la confiabilidad y solucionar problemas de larga data, esperábamos que se produjera una regresión de rendimiento, ya que comenzamos a ejecutar un analizador completamente desarrollado. Para probar esto, creamos una comparativa que renderiza alrededor de 3,500 declaraciones de propiedades y genera perfiles de las versiones basadas en regex y en el analizador con una limitación de 6 veces en una máquina M1.

Como esperábamos, el enfoque basado en el análisis resultó ser un 27% más lento que el enfoque basado en regex para ese caso. El enfoque basado en regex tardó 11 s en renderizarse y el enfoque basado en el analizador tardó 15 s.

Teniendo en cuenta los beneficios que obtenemos del nuevo enfoque, decidimos avanzar con él.

Agradecimientos

Agradecemos de corazón a Sofia Emelianova y Jecelyn Yeen por su invaluable ayuda para editar esta publicación.

Descarga los canales de vista previa

Considera usar Chrome Canary, Dev o Beta como tu navegador de desarrollo predeterminado. Estos canales de versión preliminar te brindan acceso a las funciones más recientes de DevTools, te permiten probar las APIs de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que tus usuarios.

Comunícate con el equipo de Chrome DevTools

Usa las siguientes opciones para hablar sobre las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con DevTools.