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

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Hai notato le proprietà CSS in Chrome DevTools La scheda Stili ha un aspetto un po' più curato ultimamente? 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 l'attuale DevTools con la versione precedente:

In alto: è l'ultima versione di Chrome, In basso: Chrome 121.

Che differenza c'è, vero? Di seguito sono riportati i miglioramenti principali:

  • color-mix. Una pratica anteprima che rappresenta visivamente i due argomenti relativi ai colori all'interno della funzione color-mix.
  • pink. Un'anteprima 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]). Migliorata la gestione delle variabili non definite, con la variabile non definita visualizzata in grigio e il valore di riserva attivo (in questo caso, un colore HSL) visualizzato con un'anteprima colore cliccabile.
  • hsl(…): un'altra anteprima colore cliccabile per la funzione colore hsl, che consente di accedere rapidamente al selettore colori.
  • 177deg: un orologio ad angolo 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 raggiungere questo obiettivo, abbiamo dovuto insegnare a DevTools a comprendere i valori delle proprietà CSS molto meglio rispetto a prima.

Queste anteprime non erano già disponibili?

Anche se queste icone di anteprima possono sembrare familiari, non sono sempre state visualizzate in modo coerente, soprattutto con una sintassi CSS complessa, come nell'esempio sopra. Anche nei casi in cui l'azienda lavorava, spesso è stato necessario un notevole impegno per farle funzionare correttamente.

Il motivo è che il sistema di analisi dei valori è cresciuto in modo organico sin 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 ha richiesto una riprogettazione completa 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 loro relazioni. Ad esempio, nella dichiarazione border: 1px solid red, verrebbe riconosciuto 1px come lunghezza, solid come stringa e red come colore.
  2. Rendering. Basandosi sull'analisi strutturale, la fase di rendering trasforma questi componenti in una rappresentazione HTML. In questo modo, il testo della proprietà visualizzata viene arricchito con elementi interattivi e segnali visivi. Ad esempio, il valore del colore red viene visualizzato con un'icona colore cliccabile che, se selezionata, rivela un selettore colori per semplificare la modifica.

Espressioni regolari

In precedenza, ci siamo basati su espressioni regolari (regex) per analizzare in dettaglio i valori delle proprietà ai fini dell'analisi strutturale. Abbiamo mantenuto un elenco di regex per abbinare i bit dei valori delle proprietà che abbiamo considerato come decorare. Ad esempio, esistono espressioni che corrispondono a colori, lunghezze e angoli CSS, sottoespressioni più complesse come chiamate di funzione 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 alla parte successiva del testo.

Sebbene questo sistema funzionasse bene per la maggior parte del tempo, il numero di casi in cui non è continuato a crescere. Nel corso degli anni abbiamo ricevuto un buon numero di segnalazioni di bug in cui la corrispondenza non era corretta. A mano a mano che le abbiamo sistemate, alcune soluzioni semplici, altre piuttosto elaborate, abbiamo dovuto ripensare il nostro approccio per tenere a bada il nostro debito tecnico. Diamo un'occhiata ad alcuni dei problemi.

Corrispondenza di color-mix()

L'espressione regolare che abbiamo utilizzato per la funzione color-mix() era la seguente:

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

che corrisponde alla sua sintassi:

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

Prova a eseguire l'esempio seguente 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);

Corrisponde al risultato della funzione color-mix.

L'esempio più semplice va bene. Tuttavia, nell'esempio più complesso, la corrispondenza <firstColor> è hsl(177deg var(--saturation, mentre la corrispondenza <secondColor> è 100%) 50%)), che è completamente priva di significato.

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

Corrispondenza di tan()

Uno dei bug più esilaranti segnalati riguardava la funzione trigonometrica tan() . L'espressione regolare che stavamo utilizzando per la corrispondenza dei colori includeva una sottoespressione \b[a-zA-Z]+\b(?!-) per la corrispondenza dei colori con nome come la parola chiave red. Poi abbiamo controllato se la parte corrispondente è in realtà un colore denominato e indovina un po': anche tan è un colore denominato. Pertanto, abbiamo interpretato erroneamente le espressioni tan() come colori.

Corrispondenza di var()

Esaminiamo un altro esempio, le funzioni var() con una riserva che contiene altri riferimenti var(): var(--non-existent, var(--margin-vertical)).

La nostra espressione regolare per var() corrisponde volentieri a questo valore. Tuttavia, la corrispondenza smetterà di corrispondere alla prima parentesi di chiusura. Il testo riportato sopra corrisponde quindi a var(--non-existent, var(--margin-vertical). Si tratta di una limitazione prevista dalle regole di ricerca per la corrispondenza tramite espressione regolare. Le lingue che richiedono la corrispondenza delle parentesi non sono generalmente regolari.

Transizione a un parser CSS

Quando l'analisi del testo utilizzando espressioni regolari smette di funzionare (perché il linguaggio analizzato non è regolare), c'è un passaggio successivo canonico: utilizzare un parser per una grammatica di tipo superiore. Per i CSS, significa un parser per le lingue senza contesto. Infatti, un simile sistema di parser esisteva già nel codebase DevTools: Lezer di CodeMirror, che è la base, ad esempio, dell'evidenziazione della sintassi in CodeMirror, l'editor che trovi nel riquadro Origini. Il parser CSS di Lezer ci ha permesso di produrre alberi di sintassi (non astratte) per le regole CSS ed era pronto per l'uso. Vittoria.

Un albero di sintassi per il valore della proprietà &quot;hsl(177deg var(--saturation, 100%) 50%)&quot;. È una versione semplificata del risultato prodotto dall&#39;analizzatore sintattico di Lezer, che esclude nodi puramente sintattici per virgole e parentesi.

Tranne che per l'uso immediato, abbiamo riscontrato che non era possibile migrare direttamente dalla corrispondenza basata su regex a quella basata su parser: i due approcci funzionano da direzioni opposte. Quando abbina parti di valori con espressioni regolari, DevTools analizza l'input da sinistra a destra, cercando ripetutamente di trovare la prima corrispondenza da un elenco ordinato di pattern. Con un albero della sintassi, la corrispondenza inizia dal basso verso l'alto, ad esempio analizzando gli argomenti di una chiamata prima di cercare di trovare corrispondenze con la chiamata di funzione. È una sorta di valutazione di un'espressione aritmetica, in cui considereresti prima espressioni tra parentesi, poi operatori moltiplicativi e infine operatori additivi. In questa inquadratura, la corrispondenza basata su regex corrisponde alla valutazione dell'espressione aritmetica da sinistra a destra. Non volevamo riscrivere l'intero sistema di corrispondenza da zero: esistevano 15 diversi matcher e coppie di renderer, con migliaia di righe di codice, il che rendeva improbabile che avremmo potuto spedirlo 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 proviamo a far corrispondere le sottoespressioni dal basso verso l'alto (eseguendo il flusso dell'espressione regolare), mentre nella seconda fase il rendering viene eseguito dall'alto verso il basso. 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 in modo più o meno preciso ed esclusivo ciò che dice in copertina. Attraversiamo l'albero in ordine dal basso verso l'alto e cerchiamo di trovare una corrispondenza tra le sottoespressioni in ogni nodo dell'albero della sintassi visitato. Per trovare una corrispondenza con una sottoespressione specifica, un matcher può utilizzare un'espressione regolare proprio come faceva nel sistema esistente. A partire dalla versione 128, lo facciamo ancora in alcuni casi, ad esempio per le lunghezze corrispondenti. In alternativa, un matcher può analizzare la struttura del sottoalbero radicato nel nodo attuale. In questo modo riesce a rilevare gli errori di sintassi e a registrare contemporaneamente informazioni strutturali.

Considera l'esempio dell'albero della sintassi riportato sopra:

Fase 1: corrispondenza dal basso nell&#39;albero della sintassi.

Per questo albero, i nostri matcher vengono applicati nel seguente ordine:

  1. hsl(177degvar(--saturation, 100%) 50%): anzitutto, scopriamo il primo argomento della chiamata di funzione hsl, l'angolo di tonalità. Lo abbiniamo con uno strumento per compensare gli angoli, in modo da poter decorare il valore dell'angolo con l'apposita icona.
  2. hsl(177degvar(--saturation, 100%)50%): in secondo luogo, troviamo la chiamata di funzione var con un matcher var. Per queste chiamate vogliamo principalmente fare due cose:
    • Cerca la dichiarazione della variabile e calcolane il valore, quindi aggiungi un link e un popover al nome della variabile per collegarla, rispettivamente.
    • Decora la chiamata con un'icona colore se il valore calcolato è un colore. In realtà c'è una terza cosa, ma ne parleremo più avanti.
  3. hsl(177deg var(--saturation, 100%) 50%): infine, associamo l'espressione di chiamata per la funzione hsl in modo da poterla decorare con l'icona del colore.

Oltre a cercare le sottoespressioni da decorare, c'è una seconda funzionalità in esecuzione come parte del processo di corrispondenza. Tieni presente che nel passaggio 2 abbiamo detto di cercare il valore calcolato per il nome di una variabile. Facciamo un ulteriore passo in avanti e propaghiamo i risultati nell’albero. E non solo per la variabile, ma anche per il valore di riserva. È garantito che quando visiti un nodo della funzione var, i relativi elementi secondari sono stati visitati in anticipo, quindi conosciamo già i risultati di qualsiasi funzione var che potrebbe essere visualizzata nel valore di riserva. Di conseguenza, siamo in grado di sostituire in modo semplice ed economico le funzioni var con i relativi risultati, il che ci consente di rispondere in modo banale a domande come "Il risultato di questo var è chiamato colore?", come abbiamo fatto nel passaggio 2.

Fase 2: rendering dall'alto verso il basso

Per la seconda fase, invertiamo la direzione. Prendendo in considerazione i risultati delle corrispondenze della fase 1, visualizziamo l'albero in HTML attraversandolo in ordine dall'alto verso il basso. Verifichiamo che ogni nodo visitato corrisponda e, in tal caso, chiamiamo il renderer corrispondente del matcher. Evitiamo la necessità di una gestione speciale per i nodi che contengono solo testo (come NumberLiteral "50%") includendo un matcher e un renderer predefiniti per i nodi di testo. I renderer generano semplicemente nodi HTML che, se messi insieme, 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.

Nell'albero di esempio, questo è l'ordine in cui viene visualizzato il valore della proprietà:

  1. Visita la chiamata di funzione hsl. Corrispondeva, quindi chiama il renderer della funzione di colore. Svolge due operazioni:
    • 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 degli elementi secondari di CallExpression. In questo modo viene eseguito automaticamente il rendering del nome della funzione, delle parentesi e delle virgole, che sono solo testo.
  2. Visita il primo argomento della chiamata hsl. Ha una corrispondenza, quindi chiama il renderer angolare, che disegna l'icona dell'angolo e il testo dell'angolo.
  3. Visita il secondo argomento, ovvero la chiamata var. Una corrispondenza corrisponde, 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 e poi il rendering ricorsivo del valore di riserva.
    • Una parentesi chiusa.
  4. Visita 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 gli elementi secondari 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. Per quanto riguarda l'albero della sintassi, sarebbe responsabile del rendering dell'intero sottoalbero e il suo risultato (un nodo HTML) potrebbe essere collegato in modo chiaro al processo di rendering circostante. Questo ci ha dato la possibilità di portare matcher e renderer in coppia e scambiarli uno alla volta.

Un'altra interessante caratteristica dei renderer che controllano il rendering degli elementi secondari dei nodi corrispondenti è che ci dà la possibilità 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 degli angoli tramite questa icona e modifica l'angolo, ora siamo in grado di aggiornare il colore dell'icona del colore in tempo reale:

Come puoi vedere nell'esempio precedente, utilizziamo questo meccanismo anche per altri accoppiamenti di icone, ad esempio per color-mix() e i suoi due canali colore oppure per funzioni var che restituiscono un colore dal relativo canale di riserva.

Impatto sulle prestazioni

Durante l'analisi di questo problema per migliorare l'affidabilità e risolvere problemi di lunga data, ci aspettavamo una regressione delle prestazioni considerando che abbiamo iniziato a eseguire un parser completo. Per testare questo aspetto, abbiamo creato un benchmark che mostra circa 3500 dichiarazioni di proprietà e profilato sia la versione basata su regex che quella basata su parser con una limitazione di 6 volte su una macchina M1.

Come previsto, l'approccio basato sull'analisi si è rivelato più lento del 27% rispetto a quello basato su regex in quel caso. L'approccio basato su regex ha impiegato 11 secondi per il rendering e l'approccio basato su parser ha impiegato 15 secondi per il rendering.

Considerando i successi che otteniamo dal nuovo approccio, abbiamo deciso di procedere.

Ringraziamenti

La nostra più profonda gratitudine va a Sofia Emelianova e Jecelyn Yeen per il loro inestimabile aiuto nella modifica di questo post.

Scaricare i canali in anteprima

Prendi in considerazione l'utilizzo di Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle funzionalità più recenti di DevTools, testare le API delle piattaforme web all'avanguardia e individuare i problemi sul tuo sito prima che lo facciano gli utenti.

Contattare il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere delle nuove funzionalità e modifiche nel post o di qualsiasi altra informazione relativa a DevTools.

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema di DevTools utilizzando Altre opzioni   Altro > Guida > Segnala un problema di DevTools in DevTools.
  • Invia un tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sulle novità nei video di YouTube di DevTools o nei video di YouTube dei suggerimenti di DevTools.