Oltre le espressioni regolari: miglioramento dell'analisi del valore CSS in Chrome DevTools

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Hai notato che ultimamente le proprietà CSS nella scheda Stili di Chrome DevTools sembrano un po' più raffinate? Questi aggiornamenti, implementati tra le versioni 121 e 128 di Chrome, sono il risultato di un miglioramento significativo nella modalità di analisi e presentazione dei valori CSS. In questo articolo illustreremo i dettagli tecnici di questa trasformazione, passando da un sistema di corrispondenza delle espressioni regolari a un parser più affidabile.

Confrontiamo gli attuali DevTools con la versione precedente:

In alto: si tratta dell'ultima versione di Chrome. In basso: Chrome 121.

Che differenza c'è, vero? Ecco una suddivisione dei principali miglioramenti:

  • color-mix. Un'utile anteprima che rappresenta visivamente i due argomenti di colore all'interno della funzione color-mix.
  • pink. Un'anteprima del colore cliccabile per il colore denominato pink. Fai clic su questo pulsante per aprire un selettore colori e apportare facilmente le modifiche.
  • var(--undefined, [fallback value]). È stata migliorata la gestione delle variabili non definite, con la variabile non definita disattivata e il valore di riserva attivo (in questo caso, un colore HSL) visualizzato con un'anteprima del colore cliccabile.
  • hsl(…): un'altra anteprima colore cliccabile per la funzione colore hsl, che consente di accedere rapidamente al selettore colori.
  • 177deg: un orologio angolare cliccabile che ti consente di trascinare e modificare in modo interattivo il valore dell'angolo.
  • var(--saturation, …): un link cliccabile alla definizione della proprietà personalizzata, che consente di passare facilmente alla dichiarazione pertinente.

La differenza è sorprendente. Per farlo, abbiamo dovuto insegnare a DevTools a comprendere i valori delle proprietà CSS molto meglio di prima.

Queste anteprime non erano già disponibili?

Anche se queste icone di anteprima possono sembrare familiari, non sono sempre state mostrate in modo coerente, soprattutto con una sintassi CSS complessa, come nell'esempio sopra. Anche nei casi in cui funzionavano, spesso era necessario uno sforzo significativo per farli funzionare correttamente.

Il motivo è che il sistema di analisi dei valori è cresciuto in modo organico fin dai primi giorni di DevTools. Tuttavia, non è riuscita a stare al passo con le nuove, incredibili funzionalità che otteniamo dai CSS e con il corrispondente aumento della complessità del linguaggio. Il sistema richiedeva un completo ridisign per stare al passo con l'evoluzione ed è esattamente quello che abbiamo fatto.

Come vengono elaborati i valori delle proprietà CSS

In DevTools, la procedura di rendering e decorazione delle dichiarazioni delle proprietà nella scheda Stili è suddivisa in due fasi distinte:

  1. Analisi strutturale. Questa fase iniziale analizza la dichiarazione della proprietà per identificare i componenti sottostanti e le relative relazioni. Ad esempio, nella dichiarazione border: 1px solid red, riconoscerebbe 1px come lunghezza, solid come stringa e red come colore.
  2. Rendering. Sulla base dell'analisi strutturale, la fase di rendering trasforma questi componenti in una rappresentazione HTML. In questo modo, il testo della proprietà visualizzato viene arricchito con elementi interattivi e indicatori visivi. Ad esempio, il valore di colore red viene visualizzato con un'icona di colore cliccabile che, quando viene selezionata, mostra un selettore di colori per una facile modifica.

Espressioni regolari

In precedenza, utilizzavamo le espressioni regolari (regex) per analizzare i valori delle proprietà per l'analisi strutturale. Abbiamo mantenuto un elenco di espressioni regolari per trovare corrispondenze con i bit dei valori delle proprietà che abbiamo considerato da decorare. Ad esempio, erano presenti espressioni che corrispondevano a colori, lunghezze, angoli CSS, sottoespressioni più complicate come chiamate di funzioni var e così via. Abbiamo scansionato il testo da sinistra a destra per eseguire l'analisi del valore, cercando continuamente la prima espressione dell'elenco che corrisponde al tratto di testo successivo.

Sebbene questa soluzione abbia funzionato bene la maggior parte delle volte, il numero di casi in cui non ha funzionato ha continuato a crescere. Nel corso degli anni abbiamo ricevuto un buon numero di segnalazioni di bug in cui la corrispondenza non è andata a buon fine. Man mano che le correggevamo, alcune semplici, altre piuttosto elaborate, abbiamo dovuto ripensare il nostro approccio per tenere a bada il debito tecnico. Vediamo alcuni dei problemi.

Corrispondenza color-mix()

La regex che abbiamo utilizzato per la funzione color-mix() era la seguente:

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

Che corrisponde alla sintassi:

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

Prova a eseguire il seguente esempio per visualizzare le corrispondenze.

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);

Risultato della corrispondenza per la funzione di miscelazione dei colori.

L'esempio più semplice va bene. Tuttavia, nell'esempio più complesso, la corrispondenza <firstColor> è hsl(177deg var(--saturation e la corrispondenza <secondColor> è 100%) 50%)), il che non ha alcun significato.

Sapevamo che si trattava di un problema. Dopotutto, il CSS come linguaggio formale non è regolare, quindi abbiamo già incluso un trattamento speciale per gestire argomenti di funzioni più complicati, come le funzioni var. Tuttavia, come puoi vedere nel primo screenshot, il problema non ha funzionato comunque in tutti i casi.

Corrispondenza tan()

Uno dei bug più divertenti segnalati riguardava la funzione trigonometrica tan() . L'espressione regolare che utilizzavamo per trovare corrispondenze tra i colori includeva una sottoespressione \b[a-zA-Z]+\b(?!-) per trovare corrispondenze tra i colori denominati, come la parola chiave red. Poi abbiamo controllato se la parte corrispondente è effettivamente un colore denominato e, indovinate un po', anche tan è un colore denominato. Di conseguenza, abbiamo interpretato erroneamente le espressioni tan() come colori.

Corrispondenza var()

Esaminiamo un altro esempio, le funzioni var() con un elemento di riserva contenente altri riferimenti var(): var(--non-existent, var(--margin-vertical)).

La nostra espressione regolare per var() corrisponde volentieri a questo valore. Tranne, la corrispondenza smetterà di corrispondere alla prima parentesi di chiusura. Pertanto, il testo riportato sopra corrisponde a var(--non-existent, var(--margin-vertical). Si tratta di una limitazione classica della corrispondenza delle espressioni regolari. I linguaggi che richiedono parentesi di corrispondenza non sono fondamentalmente regolari.

Transizione a un analizzatore sintattico CSS

Quando l'analisi del testo mediante espressioni regolari non funziona (perché il linguaggio analizzato non è regolare), esiste un passaggio successivo canonico: utilizzare un parser per una grammatica di tipo superiore. Per il CSS, si tratta di un parser per i linguaggi privi di contesto. In realtà, un sistema di analisi del codice di questo tipo esisteva già nella base di codice di DevTools: Lezer di CodeMirror, che è la base, ad esempio, dell'evidenziazione della sintassi in CodeMirror, l'editor che trovi nel riquadro Origini. L'analizzatore CSS di Lezer ci ha permesso di produrre alberi sintattici (non astratti) per le regole CSS ed era pronto per essere utilizzato. Vittoria.

Un albero sintattico per il valore della proprietà &quot;hsl(177deg var(--saturation, 100%) 50%)&quot;. Si tratta di una versione semplificata del risultato prodotto dal parser Lezer, che esclude i nodi puramente sintattici per le virgole e le parentesi tonde.

Tuttavia, abbiamo riscontrato che non è possibile eseguire la migrazione direttamente dalla corrispondenza basata su regex a quella basata su parser: i due approcci funzionano in direzioni opposte. Quando abbinava parti di valori con espressioni regolari, DevTools analizzava l'input da sinistra a destra, cercando ripetutamente la prima corrispondenza da un elenco ordinato di pattern. Con un albero sintattico, la corrispondenza inizierebbe dal basso verso l'alto, ad esempio analizzando prima gli argomenti di una chiamata, prima di provare a trovare una corrispondenza con la chiamata di funzione. Pensa alla valutazione di un'espressione aritmetica, in cui devi prima considerare le espressioni tra parentesi, poi gli operatori moltiplicativi e infine gli operatori additivi. In questo contesto, la corrispondenza basata su espressioni regolari corrisponde alla valutazione dell'espressione aritmetica da sinistra a destra. Non volevamo davvero riscrivere da zero l'intero sistema di corrispondenza: esistevano 15 coppie di matcher e renderer diverse, con migliaia di righe di codice, il che rendeva improbabile che potessimo rilasciarlo in un unico traguardo.

Abbiamo quindi trovato una soluzione che ci consentisse di apportare modifiche incrementali, che descriveremo più dettagliatamente di seguito. In breve, abbiamo mantenuto l'approccio in due fasi, ma nella prima fase cerchiamo di abbinare le sottoespressioni dal basso verso l'alto (rompendo così il flusso della regex) e nella seconda fase eseguiamo il rendering dal basso verso l'alto. In entrambe le fasi, abbiamo potuto utilizzare i matcher e i rendering basati su regex esistenti, praticamente invariati, e siamo quindi stati in grado di eseguirne la migrazione uno alla volta.

Fase 1: corrispondenza dal basso verso l'alto

La prima fase fa più o meno esattamente ed esclusivamente ciò che dice la copertina. Percorriamo l'albero in ordine dal basso verso l'alto e cerchiamo di abbinare le sottoespressioni in ogni nodo dell'albero sintattico che visitiamo. Per trovare una corrispondenza con una sottoespressione specifica, un matcher potrebbe utilizzare l'espressione regolare come nel sistema esistente. A partire dalla versione 128, lo facciamo ancora in alcuni casi, ad esempio per le lunghezze corrispondenti. In alternativa, un'espressione di corrispondenza può analizzare la struttura del sottoalbero con radice nel nodo corrente. In questo modo, è possibile rilevare gli errori di sintassi e registrare contemporaneamente le informazioni strutturali.

Considera l'esempio di albero sintattico riportato sopra:

Fase 1: corrispondenza dal basso verso l&#39;alto nell&#39;albero sintattico.

Per questo albero, i nostri corrispondenti verranno applicati nel seguente ordine:

  1. hsl(177degvar(--saturation, 100%) 50%): innanzitutto, scopriamo il primo argomento della chiamata alla funzione hsl, l'angolo di tonalità. Lo abbiniamo a un'espressione di corrispondenza dell'angolo, in modo da poter decorare il valore dell'angolo con l'icona dell'angolo.
  2. hsl(177degvar(--saturation, 100%)50%): in secondo luogo, rileviamo la chiamata alla funzione var con un selettore var. Per queste chiamate vogliamo fare principalmente due cose:
    • Cerca la dichiarazione della variabile e calcola il relativo valore, quindi aggiungi un link e un popup al nome della variabile per collegarti rispettivamente a questi elementi.
    • Decora la chiamata con un'icona colore se il valore calcolato è un colore. In realtà c'è un'altra cosa, ma ne parleremo più avanti.
  3. hsl(177deg var(--saturation, 100%) 50%): infine, assoceremo l'espressione di chiamata per la funzione hsl, in modo da poterla decorare con l'icona di colore.

Oltre a cercare le sottoespressioni da decorare, nell'ambito del processo di corrispondenza viene eseguita una seconda funzionalità. Tieni presente che nel passaggio 2 abbiamo detto di cercare il valore calcolato per il nome di una variabile. In realtà, facciamo un ulteriore passo avanti e propaghiamo i risultati verso l'alto dell'albero. E non solo per la variabile, ma anche per il valore di riserva. È garantito che, quando viene visitato un nodo di funzione var, i relativi nodi secondari sono stati visitati in precedenza, quindi conosciamo già i risultati di eventuali funzioni var che potrebbero apparire nel valore di riserva. Di conseguenza, possiamo sostituire facilmente e a basso costo le funzioni var con i relativi risultati al volo, il che ci consente di rispondere facilmente a domande come "Il risultato di questa chiamata var è un colore?", come abbiamo fatto nel passaggio 2.

Fase 2: rendering dall'alto verso il basso

Per la seconda fase, invertiamo la direzione. Prendendo i risultati della corrispondenza della fase 1, rendiamo l'albero in HTML attraversandolo in ordine dall'alto verso il basso. Verifichiamo che per ogni nodo visitato corrisponda e, in tal caso, chiamiamo il renderer corrispondente del matcher. Evitiamo la necessità di un trattamento speciale per i nodi che contengono solo testo (come NumberLiteral "50%") includendo un corrispettivo e un visualizzatore predefiniti per i nodi di testo. I renderer semplicemente emettono nodi HTML che, se combinati, producono la rappresentazione del valore della proprietà incluse le relative decorazioni.

Fase 2: rendering dall&#39;alto verso il basso nell&#39;albero della sintassi.

Per l'albero di esempio, ecco l'ordine in cui viene visualizzato il valore della proprietà:

  1. Vai alla chiamata della funzione hsl. Corrispondeva, quindi chiama il renderer della funzione di colore. Svolge due funzioni:
    • Calcola il valore effettivo del colore utilizzando il meccanismo di sostituzione immediata per qualsiasi argomento var, poi disegna un'icona del colore.
    • Esegue il rendering ricorsivo dei figli di CallExpression. In questo modo, il rendering del nome della funzione, delle parentesi e delle virgole, che sono solo testo, viene eseguito automaticamente.
  2. Consulta il primo argomento della chiamata hsl. La corrispondenza è stata trovata, quindi chiama il visualizzatore dell'angolo, che disegna l'icona dell'angolo e il testo dell'angolo.
  3. Visita il secondo argomento, ovvero la chiamata var. La corrispondenza è stata trovata, quindi chiama la variabile renderer, che restituisce quanto segue:
    • Il testo var( all'inizio.
    • Il nome della variabile e la decora con un link alla definizione della variabile o con un colore di testo grigio a indicare che non era definita. Aggiunge inoltre un popover alla variabile per mostrare informazioni sul suo valore.
    • La virgola esegue quindi il rendering ricorsivo del valore di riserva.
    • Una parentesi chiusa.
  4. Consulta l'ultimo argomento della chiamata hsl. Non corrispondeva, quindi visualizzane solo i contenuti di testo.

Hai notato che in questo algoritmo, un rendering controlla completamente il modo in cui vengono visualizzati i figli di un nodo corrispondente? Il rendering ricorsivo dei bambini è proattivo. Questo trucco è ciò che ha permesso una migrazione graduale dal rendering basato su regex al rendering basato su albero della sintassi. Per i nodi che corrispondono a un'espressione regex-matcher precedente, è possibile utilizzare il renderer corrispondente nella sua forma originale. In termini di albero sintattico, assumerebbe la responsabilità del rendering dell'intero sottoalbero e il relativo risultato (un nodo HTML) potrebbe essere collegato in modo pulito al processo di rendering circostante. In questo modo abbiamo avuto la possibilità di eseguire il porting di matcher e renderer a coppie e sostituirli uno alla volta.

Un'altra interessante funzionalità dei renderer che controllano il rendering dei figli del nodo corrispondente è che ci consente di ragionare sulle dipendenze tra le icone che stiamo aggiungendo. Nell'esempio precedente, il colore prodotto dalla funzione hsl dipende ovviamente dal valore della tonalità. Ciò significa che il colore mostrato dall'icona dipende dall'angolo mostrato dall'icona. Se l'utente apre l'editor dell'angolo tramite l'icona e modifica l'angolo, ora possiamo aggiornare il colore dell'icona del colore in tempo reale:

Come puoi vedere nell'esempio riportato sopra, utilizziamo questo meccanismo anche per altri accoppiamenti di icone, ad esempio per color-mix() e i suoi due canali di colore o le funzioni var che restituiscono un colore dal valore predefinito.

Impatto sulle prestazioni

Quando abbiamo esaminato questo problema per migliorare l'affidabilità e risolvere problemi di lunga data, ci aspettavamo una certa regressione delle prestazioni, considerando che abbiamo iniziato a eseguire un parser completo. Per testare questa funzionalità, abbiamo creato un benchmark che esegue il rendering di circa 3500 dichiarazioni di proprietà e ha profilato sia le versioni basate su regex che quelle basate su parser con un throttling sei volte superiore su una macchina M1.

Come previsto, l'approccio basato sull'analisi sintattica è risultato essere il 27% più lento dell'approccio basato sulle regex per questo caso. L'approccio basato su regex ha richiesto 11 secondi per il rendering, mentre l'approccio basato su parser ha richiesto 15 secondi.

Dati i vantaggi che otteniamo dal nuovo approccio, abbiamo deciso di procedere.

Ringraziamenti

La nostra gratitudine più profonda va a Sofia Emelianova e Jecelyn Yeen per la loro preziosa assistenza nella modifica di questo post.

Scaricare i canali di anteprima

Valuta la possibilità di utilizzare Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle funzionalità più recenti di DevTools, di testare API di piattaforme web all'avanguardia e di trovare i problemi sul tuo sito prima che lo facciano gli utenti.

Contatta il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere di nuove funzionalità, aggiornamenti o qualsiasi altro argomento relativo a DevTools.