Além das expressões regulares: melhoria da análise de valores CSS no Chrome DevTools

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Você notou que as propriedades CSS na guia Styles do Chrome DevTools estão um pouco mais refinadas ultimamente? Essas atualizações, lançadas entre o Chrome 121 e 128, resultaram de uma melhoria significativa na forma como analisamos e apresentamos os valores CSS. Neste artigo, abordaremos os detalhes técnicos dessa transformação, passando de um sistema de correspondência de expressões regulares para um analisador mais robusto.

Vamos comparar o DevTools atual com a versão anterior:

Parte de cima: é a versão mais recente do Chrome. Parte de baixo: Chrome 121.

Uma grande diferença, não é mesmo? Estas são as principais melhorias:

  • color-mix: uma visualização prática que representa visualmente os dois argumentos de cor na função color-mix.
  • pink: uma visualização de cor clicável para a cor nomeada pink. Clique nele para abrir um seletor de cores e fazer ajustes fáceis.
  • var(--undefined, [fallback value]). Melhoria no processamento de variáveis indefinidas, com a variável indefinida esmaecida e o valor de fallback ativo (neste caso, uma cor HSL) exibido com uma visualização de cor clicável.
  • hsl(…): outra visualização de cor clicável para a função de cor hsl, que oferece acesso rápido ao seletor de cores.
  • 177deg: um relógio angular clicável que permite arrastar e modificar o valor do ângulo de maneira interativa.
  • var(--saturation, …): um link clicável para a definição da propriedade personalizada, facilitando a navegação até a declaração relevante.

A diferença é impressionante. Para isso, tivemos que ensinar o DevTools a entender os valores de propriedade CSS muito melhor do que antes.

Essas prévias já não estavam disponíveis?

Embora esses ícones de visualização possam parecer familiares, eles nem sempre são exibidos de forma consistente, especialmente em sintaxe CSS complexa, como o exemplo acima. Mesmo nos casos em que elas funcionavam, muitas vezes era necessário um esforço significativo para que elas funcionassem corretamente.

O motivo é que o sistema de análise de valores tem crescido organicamente desde os primeiros dias do DevTools. No entanto, ele não conseguiu acompanhar os novos recursos incríveis que estamos recebendo do CSS e o aumento correspondente na complexidade da linguagem. O sistema exigiu um redesenho completo para acompanhar a evolução, e foi exatamente isso que fizemos.

Como os valores da propriedade CSS são processados

Nas Ferramentas do desenvolvedor, o processo de renderização e decoração de declarações de propriedade na guia Styles é dividido em duas fases distintas:

  1. Análise estrutural. Essa fase inicial disseca a declaração de propriedade para identificar os componentes subjacentes e as relações deles. Por exemplo, na declaração border: 1px solid red, ele reconheceria 1px como um comprimento, solid como uma string e red como uma cor.
  2. Renderização. Com base na análise estrutural, a fase de renderização transforma esses componentes em uma representação HTML. Isso enriquece o texto da propriedade exibida com elementos interativos e dicas visuais. Por exemplo, o valor de cor red é renderizado com um ícone de cor clicável que, quando clicado, revela um seletor de cores para facilitar a modificação.

Expressões regulares

Antes, usávamos expressões regulares (regexes) para analisar os valores de propriedade na análise estrutural. Mantivemos uma lista de expressões regulares para corresponder aos bits dos valores de propriedade que consideramos decorar. Por exemplo, havia expressões que correspondiam a cores, comprimentos, ângulos e subexpressões mais complicadas do CSS, como chamadas de função var, e assim por diante. Analisamos o texto da esquerda para a direita para fazer uma análise de valor, continuamente procurando a primeira expressão na lista que corresponde ao próximo trecho do texto.

Embora isso funcionasse bem na maioria das vezes, o número de casos em que não funcionava continuava crescendo. Ao longo dos anos, recebemos vários relatórios de bugs em que a correspondência não estava correta. Ao corrigirmos esses problemas, alguns simples e outros mais elaborados, tivemos que repensar nossa abordagem para manter a dívida técnica sob controle. Vamos conferir algumas das questões.

color-mix() correspondente

O regex usado para a função color-mix() foi:

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

Que corresponde à sintaxe:

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

Tente executar o exemplo a seguir para visualizar as correspondências.

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 da correspondência para a função de mistura de cores.

O exemplo mais simples funciona bem. No entanto, no exemplo mais complexo, a correspondência <firstColor> é hsl(177deg var(--saturation, e a correspondência <secondColor> é 100%) 50%)), o que não faz sentido.

Sabíamos que isso era um problema. Afinal, o CSS como uma linguagem formal não é regular. Por isso, já incluímos um processamento especial para lidar com argumentos de função mais complicados, como as funções var. No entanto, como você pode ver na primeira captura de tela, isso ainda não funciona em todos os casos.

tan() correspondente

Um dos bugs mais engraçados informados foi sobre a função trigonométrica tan() . A regex que estávamos usando para correspondência de cores incluía uma subexpressão \b[a-zA-Z]+\b(?!-) para correspondência de cores nomeadas, como a palavra-chave red. Em seguida, verificamos se a parte correspondente é realmente uma cor nomeada. E adivinhe só, tan também é uma cor nomeada. Portanto, interpretamos incorretamente as expressões tan() como cores.

var() correspondente

Confira outro exemplo: funções var() com um substituto que contém outras referências var(): var(--non-existent, var(--margin-vertical)).

O regex de var() corresponderia a esse valor. Exceto que ele pararia de corresponder ao primeiro parêntese de fechamento. Portanto, a correspondência do texto acima é feita como var(--non-existent, var(--margin-vertical). Essa é uma limitação de expressão regular. Linguagens que exigem parênteses correspondentes não são fundamentalmente regulares.

Fazer a transição para um analisador de CSS

Quando a análise de texto que usa expressões regulares deixa de funcionar (porque a linguagem analisada não é regular), há uma próxima etapa canônica: usar um analisador para uma gramática de tipo superior. Para CSS, isso significa um analisador para idiomas livres de contexto. Na verdade, esse sistema de parser já existia no código-base do DevTools: o Lezer do CodeMirror, que é a base, por exemplo, do realce de sintaxe no CodeMirror, o editor que você encontra no painel Sources. O analisador CSS do Lezer nos permitiu produzir árvores de sintaxe (não abstratas) para regras CSS e estava pronto para uso. Vitória.

Uma árvore de sintaxe para o valor da propriedade &quot;hsl(177deg var(--saturation, 100%) 50%)&quot;. É uma versão simplificada do resultado produzido pelo analisador Lezer, deixando de fora nós puramente sintáticos para vírgulas e parênteses.

No entanto, descobrimos que não é viável migrar diretamente da correspondência baseada em regex para a baseada em analisador: as duas abordagens funcionam em direções opostas. Ao corresponder partes de valores com expressões regulares, as Ferramentas do desenvolvedor verificavam a entrada da esquerda para a direita, tentando repetidamente encontrar a correspondência mais antiga em uma lista ordenada de padrões. Com uma árvore de sintaxe, a correspondência começa de baixo para cima, por exemplo, analisando primeiro os argumentos de uma chamada antes de tentar corresponder à chamada de função. Pense nisso como a avaliação de uma expressão aritmética, em que você considera primeiro as expressões entre parênteses, depois os operadores multiplicativos e, em seguida, os operadores aditivos. Nesse contexto, a correspondência baseada em regex corresponde à avaliação da expressão aritmética da esquerda para a direita. Não queríamos reescrever todo o sistema de correspondência do zero: havia 15 pares de matchers e renderizadores diferentes, com milhares de linhas de código, o que tornava improvável que pudéssemos enviar em um único marco.

Assim, encontramos uma solução que nos permitiu fazer alterações incrementais, que vamos descrever com mais detalhes abaixo. Resumindo, mantivemos a abordagem de duas fases, mas na primeira fase tentamos corresponder a subexpressões de baixo para cima (quebrando o fluxo de regex) e, na segunda fase, renderizamos de cima para baixo. Nas duas fases, usamos os matchers e renderizadores baseados em regex existentes, praticamente sem alterações, e conseguimos migrar um por um.

Fase 1: correspondência ascendente

A primeira fase faz, de forma mais ou menos exata e exclusiva, o que diz na capa. Percorremos a árvore em ordem, de baixo para cima, e tentamos corresponder subexpressões em cada nó da árvore de sintaxe que visitamos. Para corresponder a uma subexpressão específica, um comparador pode usar regex, assim como no sistema atual. A partir da versão 128, ainda fazemos isso em alguns casos, por exemplo, para comprimentos correspondentes. Como alternativa, um comparador pode analisar a estrutura do subárvore com raiz no nó atual. Isso permite que ele detecte erros de sintaxe e registre informações estruturais ao mesmo tempo.

Considere o exemplo de árvore de sintaxe acima:

Fase 1: correspondência de baixo para cima na árvore de sintaxe.

Para essa árvore, nossos matchers seriam aplicados na seguinte ordem:

  1. hsl(177degvar(--saturation, 100%) 50%): primeiro, descobrimos o primeiro argumento da chamada de função hsl, o ângulo de matiz. Combinamos com um correspondente de ângulo para que possamos decorar o valor do ângulo com o ícone correspondente.
  2. hsl(177degvar(--saturation, 100%)50%): em segundo lugar, descobrimos a chamada de função var com um comparador de var. Para essas chamadas, queremos fazer duas coisas:
    • Consultar a declaração da variável e calcular o valor dela, além de adicionar um link e um pop-up ao nome da variável para se conectar a eles, respectivamente.
    • Decore a chamada com um ícone de cor se o valor computado for uma cor. Na verdade, há uma terceira coisa, mas vamos falar sobre isso mais tarde.
  3. hsl(177deg var(--saturation, 100%) 50%): por fim, combinamos a expressão de chamada da função hsl para que ela possa ser decorada com o ícone de cor.

Além de procurar subexpressões que gostaríamos de decorar, há um segundo recurso que estamos executando como parte do processo de correspondência. Na etapa 2, dissemos que procuramos o valor calculado para um nome de variável. Na verdade, vamos além e propagamos os resultados para cima na árvore. E não apenas para a variável, mas também para o valor padrão. É garantido que, ao visitar um nó da função var, os filhos tenham sido visitados com antecedência. Portanto, já sabemos os resultados de qualquer função var que possa aparecer no valor substituto. Portanto, podemos substituir as funções var com os resultados de forma fácil e barata, o que nos permite responder a perguntas como "O resultado desta chamada var é uma cor?", como fizemos na etapa 2.

Fase 2: renderização de cima para baixo

Na segunda fase, invertemos a direção. Usando os resultados da correspondência da fase 1, renderizamos a árvore em HTML, percorrendo-a em ordem de cima para baixo. Para cada nó visitado, verificamos se ele corresponde e, em caso afirmativo, chamamos o renderizador correspondente do comparador. Evitamos a necessidade de tratamento especial para nós que contêm apenas texto (como o NumberLiteral "50%") incluindo um correspondente e um renderizador padrão para nós de texto. Os renderizadores simplesmente geram nós HTML que, quando reunidos, produzem a representação do valor da propriedade, incluindo as decorações.

Fase 2: renderização de cima para baixo na árvore de sintaxe.

Para a árvore de exemplo, esta é a ordem em que o valor da propriedade é renderizado:

  1. Acesse a chamada de função hsl. Ele correspondeu, então chame o renderizador da função de cor. Ele faz duas coisas:
    • Calcula o valor real da cor usando o mecanismo de substituição em tempo real para todos os argumentos var e exibe um ícone de cor.
    • Renderiza recursivamente os filhos do CallExpression. Isso renderiza automaticamente o nome da função, parênteses e vírgulas, que são apenas texto.
  2. Acesse o primeiro argumento da chamada hsl. Ele correspondeu, então chame o renderizador de ângulo, que desenha o ícone e o texto do ângulo.
  3. Acesse o segundo argumento, que é a chamada var. Ele correspondeu, então chame a variável renderer, que vai gerar o seguinte:
    • O texto var( no início.
    • O nome da variável e a decora com um link para a definição da variável ou com uma cor de texto cinza para indicar que ela foi indefinida. Também adiciona um popover à variável para mostrar informações sobre o valor dela.
    • A vírgula renderiza recursivamente o valor substituto.
    • Um parêntese de fechamento.
  4. Acesse o último argumento da chamada hsl. Como não há correspondência, apenas mostre o conteúdo de texto.

Você notou que, nesse algoritmo, uma renderização controla totalmente como os filhos de um nó correspondente são renderizados? Renderizar recursivamente os filhos é proativo. Esse truque permitiu uma migração gradual da renderização baseada em regex para a renderização baseada em árvore de sintaxe. Para nós correspondentes a um comparador de regex legado, o renderizador correspondente pode ser usado na forma original. Em termos de árvore de sintaxe, ele seria responsável por renderizar todo o subárvore, e o resultado dele (um nó HTML) poderia ser conectado de forma limpa ao processo de renderização. Isso nos deu a opção de transferir pares de matchers e renderizadores e trocá-los um por um.

Outro recurso legal dos renderizadores que controlam a renderização das crianças do nó correspondente é que ele nos permite raciocinar sobre as dependências entre os ícones que estamos adicionando. No exemplo acima, a cor produzida pela função hsl obviamente depende do valor de matiz dela. Isso significa que a cor mostrada pelo ícone de cor depende do ângulo mostrado pelo ícone de ângulo. Se o usuário abrir o editor de ângulos usando esse ícone e modificar o ângulo, vamos poder atualizar a cor do ícone em tempo real:

Como mostrado no exemplo acima, também usamos esse mecanismo para outros pares de ícones, como para color-mix() e seus dois canais de cores ou para funções var que retornam uma cor do substituto.

Impacto no desempenho

Ao analisar esse problema para melhorar a confiabilidade e corrigir problemas de longa data, esperávamos uma regressão de desempenho, considerando que começamos a executar um analisador completo. Para testar isso, criamos um comparativo que renderiza cerca de 3,5 mil declarações de propriedade e gera perfis das versões baseadas em regex e em parser com limitação de 6x em uma máquina M1.

Como esperado, a abordagem baseada em análise ficou 27% mais lenta do que a abordagem baseada em regex para esse caso. A abordagem baseada em regex levou 11 segundos para renderizar, e a abordagem baseada em analisador levou 15 segundos.

Considerando os benefícios da nova abordagem, decidimos seguir em frente.

Agradecimentos

Agradecemos a Sofia Emelianova e a Jecelyn Yeen pela ajuda valiosa na edição desta postagem.

Fazer o download dos canais de visualização

Considere usar o Chrome Canary, Dev ou Beta como seu navegador de desenvolvimento padrão. Esses canais de pré-lançamento dão acesso aos recursos mais recentes do DevTools, permitem testar APIs modernas da plataforma da Web e ajudam a encontrar problemas no site antes que os usuários o façam!

Entre em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir novos recursos, atualizações ou qualquer outro item relacionado ao DevTools.