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

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Hai notato che le proprietà CSS nella scheda Stili di Chrome DevTools sembrano un po' più raffinate ultimamente? Questi aggiornamenti, implementati tra Chrome 121 e 128, sono il risultato di un miglioramento significativo del modo in cui analizziamo e presentiamo i 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ù solido.

Confrontiamo la versione attuale di DevTools con la versione precedente:

In alto: si tratta della versione più recente di Chrome. In basso: Chrome 121.

Una bella differenza, 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 per aprire un selettore colori per regolazioni semplici.
  • 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 del colore cliccabile per la funzione di colore hsl, che consente di accedere rapidamente al selettore dei 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?

Sebbene queste icone di anteprima possano sembrare familiari, non sono sempre state visualizzate in modo coerente, in particolare in una sintassi CSS complessa come nell'esempio riportato 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 è stato in grado di stare al passo con le recenti straordinarie funzionalità che stiamo ottenendo dal CSS e con il conseguente 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, il processo di rendering e decorazione delle dichiarazioni delle proprietà nella scheda Styles è suddiviso 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 funziona 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'elaborazione speciale per gestire argomenti di funzioni più complicati, come le funzioni var. Tuttavia, come puoi vedere nel primo screenshot, la soluzione non ha funzionato 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()

Diamo un'occhiata a un altro esempio, le funzioni var() con un valore predefinito contenente altri riferimenti var(): var(--non-existent, var(--margin-vertical)).

La nostra regex per var() corrisponderebbe a questo valore. Tuttavia, la corrispondenza si interrompe alla prima parentesi tonda 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 tramite 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 prima consideri 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 diversi, con migliaia di righe di codice, il che rendeva improbabile che potessimo rilasciarlo in un unico traguardo.

Abbiamo quindi trovato una soluzione che ci ha permesso 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 corrispondenti e le visualizzazioni basati su espressioni regolari esistenti, praticamente invariati, e quindi eseguire 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, in alcuni casi lo facciamo ancora, 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 di 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 che cerchiamo il valore calcolato per un nome di 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 funzione var, i relativi nodi figlio sono stati visitati in precedenza, quindi sappiamo 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. Per ogni nodo visitato, controlliamo se corrisponde e, in caso affermativo, chiamiamo il renderer corrispondente del correlatore. 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. La corrispondenza è stata trovata, quindi chiama il visualizzatore della funzione di colore. Svolge due funzioni:
    • Calcola il valore di colore effettivo utilizzando il meccanismo di sostituzione dinamica per qualsiasi argomento var, quindi disegna un'icona di 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. Controlla il secondo argomento, ovvero la chiamata var. La corrispondenza è stata trovata, quindi chiama la var renderer, che restituisce quanto segue:
    • Il testo var( all'inizio.
    • Il nome della variabile e lo decora con un link alla definizione della variabile o con un colore di testo grigio per indicare che non è stato definito. Aggiunge inoltre un popup alla variabile per mostrare informazioni sul relativo valore.
    • La virgola esegue quindi il rendering ricorsivo del valore di riserva.
    • Una parentesi chiusa.
  4. Consulta l'ultimo argomento della chiamata hsl. Non c'è corrispondenza, quindi stampa 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 degli elementi secondari è proattivo. Questo trucco ha consentito una migrazione graduale dal rendering basato su regex al rendering basato sull'albero della sintassi. Per i nodi corrispondenti a un'espressione regolare precedente, il visualizzatore corrispondente potrebbe essere utilizzato 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 del colore dipende dall'angolo mostrato dall'icona dell'angolo. 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 i tuoi utenti.

Contatta il team di Chrome DevTools

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