Więcej niż wyrażenia regularne: ulepszenie analizy wartości CSS w Narzędziach deweloperskich w Chrome

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Czy zauważyłeś/zauważyłaś, że właściwości CSS na karcie Style w Narzędziach deweloperskich w Chrome wyglądają ostatnio nieco lepiej? Te aktualizacje, które zostały wdrożone między wersjami Chrome 121 i 128, są wynikiem znacznej poprawy sposobu analizowania i prezentowania wartości CSS. W tym artykule omawiamy szczegóły techniczne tej przemiany – przejścia z systemu dopasowywania wyrażeń regularnych na bardziej wydajny parsownik.

Porównaj obecne DevTools z poprzednią wersją:

Góra: najnowsza wersja Chrome, dół: Chrome 121.

Spora różnica, prawda? Oto najważniejsze ulepszenia:

  • color-mix. Przydatny podgląd wizualny przedstawiający 2 argumenty koloru w ramach funkcji color-mix.
  • pink. Klikalny podgląd koloru o nazwie pink. Kliknij, aby otworzyć selektor kolorów i łatwo dostosować kolor.
  • var(--undefined, [fallback value]). Ulepszono obsługę zdefiniowanych zmiennych. Zmienna z niezdefiniowaną wartością jest wyszarzona, a aktywne wartości zastępcze (w tym przypadku kolor HSL) są wyświetlane z możliwością kliknięcia podglądu koloru.
  • hsl(…): kolejna klikalna podglądowa próbka koloru dla funkcji hsl, która zapewnia szybki dostęp do selektora kolorów.
  • 177deg: klikalny zegar kątowy, który umożliwia interaktywne przeciąganie i modyfikowanie wartości kąta.
  • var(--saturation, …): klikalny link do definicji właściwości niestandardowej, który ułatwia przejście do odpowiedniej deklaracji.

Różnica jest uderzająca. Aby to osiągnąć, musieliśmy nauczyć DevTools znacznie lepiej rozumieć wartości właściwości CSS niż do tej pory.

Czy te podglądy nie były już dostępne?

Te ikony podglądu mogą wydawać się znajome, ale nie zawsze były wyświetlane konsekwentnie, zwłaszcza w przypadku złożonej składni CSS, jak w przykładzie powyżej. Nawet w przypadkach, gdy działały, często wymagały one znacznego wysiłku, aby działały prawidłowo.

Dzieje się tak, ponieważ system analizowania wartości rozwija się od samego początku istnienia DevTools. Nie nadąża jednak za ostatnimi nowymi funkcjami, które udostępnia nam CSS, oraz za rosnącą złożonością języka. Aby nadążyć za rozwojem, system wymagał całkowitego przeprojektowania. I tak właśnie zrobiliśmy.

Jak są przetwarzane wartości właściwości CSS

W DevTools proces renderowania i dekorowania deklaracji właściwości na karcie Style jest podzielony na 2 odrębne fazy:

  1. Analiza strukturalna. Na tym początkowym etapie analizowana jest deklaracja usługi w celu zidentyfikowania jej podstawowych komponentów i ich relacji. Na przykład w deklaracji border: 1px solid red rozpoznaje 1px jako długość, solid jako ciąg znaków, a red jako kolor.
  2. Renderowanie. Na podstawie analizy strukturalnej faza renderowania przekształca te komponenty w postać HTML. Dzięki temu wyświetlany tekst będzie zawierać interaktywne elementy i wskazówki wizualne. Na przykład wartość koloru red jest renderowana za pomocą klikalnej ikony koloru, która po kliknięciu powoduje wyświetlenie selektora kolorów umożliwiającego łatwą modyfikację.

Wyrażenia regularne

Wcześniej do analizy strukturalnej wartości właściwości używaliśmy wyrażeń regularnych. Utrzymywaliśmy listę wyrażeń regularnych, aby dopasowywać fragmenty wartości właściwości, które uznaliśmy za odpowiednie do ozdabiania. Były to na przykład wyrażenia pasujące do kolorów, długości i kątów CSS, bardziej skomplikowanych podwyrażeń, takich jak wywołania funkcji var itp. Aby przeprowadzić analizę wartości, skanowaliśmy tekst od lewej do prawej, stale szukając pierwszego wyrażenia z listy pasującego do następnego fragmentu tekstu.

Większość czasu działało to dobrze, ale liczba przypadków, w których nie działało, stale rosła. Z laty otrzymujemy sporo zgłoszeń błędów dotyczących przypadków, w których dopasowanie nie działało prawidłowo. W miarę ich naprawiania – niektóre proste, inne dość skomplikowane – musieliśmy przemyśleć nasze podejście, aby nie dopuścić do zadłużenia technicznego. Przyjrzyjmy się niektórym z nich.

Pasuje do color-mix()

Wyrażenie regularne użyte w funkcji color-mix() miało postać:

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

Składnia:

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

Aby zwizualizować dopasowania, uruchom poniższy przykład.

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

Wynik dopasowania do funkcji mieszania kolorów.

Prostszy przykład działa dobrze. Jednak w bardziej złożonym przykładzie dopasowanie <firstColor> to hsl(177deg var(--saturation, a dopasowanie <secondColor> to 100%) 50%)), co nie ma żadnego znaczenia.

Wiedzieliśmy, że to problem. W końcu CSS jako język formalny nie jest zwykłym językiem, dlatego uwzględniliśmy już specjalne przetwarzanie bardziej skomplikowanych argumentów funkcji, takich jak funkcje var. Jak widać na pierwszym zrzucie ekranu, nie zawsze się to jednak sprawdza.

Pasuje do tan()

Jednym z bardziej zabawnych błędów zgłaszanych był błąd związany z funkcją trygonometryczną tan() . Wyrażenie regularne używane do dopasowywania kolorów zawierało podwyrażenie \b[a-zA-Z]+\b(?!-) dopasowujące nazwy kolorów, takie jak słowo kluczowe red. Następnie sprawdziliśmy, czy dopasowana część jest rzeczywiście kolorem o nazwie, i okazało się, że tan to też kolor o nazwie. W związku z tym błędnie zinterpretowaliśmy wyrażenia tan() jako kolory.

Pasuje do var()

Przyjrzyjmy się innemu przykładowi: funkcja var() z wartością zastępczą zawierającą inne odwołania var(): var(--non-existent, var(--margin-vertical)).

Nasze wyrażenie regularne var() pasuje do tej wartości. Z tym, że zatrzyma się na pierwszym nawiasie klamrowym. Tekst powyżej jest dopasowany jako var(--non-existent, var(--margin-vertical). Jest to typowe ograniczenie dopasowywania wyrażeń regularnych. Języki, które wymagają dopasowania nawiasów, nie są w zasadzie regularne.

Przejście na parser CSS

Gdy analiza tekstu za pomocą wyrażeń regularnych przestaje działać (ponieważ analizowany język nie jest regularny), należy użyć parsowania dla gramatyki wyższego typu. W przypadku CSS oznacza to parsowanie dla języków bez kontekstu. Taki system parsowania istniał już w kodzie źródłowym DevTools: Lezer w CodeMirror, który stanowi podstawę np. podświetlania składni w CodeMirror, edytorze w panelu Źródła. Parser CSS Lezera pozwolił nam wygenerować (nieabstrakcyjne) drzewa składni dla reguł CSS i był gotowy do użycia. Zwycięstwo.

Drzewo składni dla wartości właściwości „hsl(177deg var(--saturation, 100%) 50%)”. Jest to uproszczona wersja wyniku wygenerowanego przez parsowanie Lezer, z pominięciem czysto syntaktycznych węzłów przecinków i nawiasów.

Okazało się jednak, że bezpośrednie przejście z dopasowywania opartego na wyrażeniach regularnych na oparte na parsowaniu nie jest możliwe, ponieważ te 2 metody działają w przeciwnych kierunkach. Podczas dopasowywania fragmentów wartości za pomocą wyrażeń regularnych DevTools skanował dane od lewej do prawej, wielokrotnie próbując znaleźć najstarsze dopasowanie na uporządkowanej liście wzorów. W przypadku drzewa składni dopasowanie rozpoczyna się od dołu do góry, np. najpierw analizowane są argumenty wywołania, a dopiero potem próbuje się dopasować wywołanie funkcji. Wyobraź sobie to jako obliczanie wyrażenia arytmetycznego, w którym najpierw uwzględniasz wyrażenia w nawiasach, potem operatory mnożenia, a na końcu operatory dodawania. W tym przypadku dopasowanie oparte na wyrażeniu regularnym odpowiada ocenie wyrażenia arytmetycznego od lewej do prawej. Nie chcieliśmy od nowa pisać całego systemu dopasowywania. Mieliśmy 15 różnych par dopasowywania i renderowania, które zawierały tysiące linii kodu, więc nie było możliwe, abyśmy mogli wprowadzić je w ramach jednego etapu.

Dlatego opracowaliśmy rozwiązanie, które pozwoliło nam wprowadzać stopniowe zmiany. Opiszemy je bardziej szczegółowo poniżej. Krótko mówiąc, zachowaliśmy podejście dwufazowe, ale w pierwszej fazie próbujemy dopasowywać podwyrażenia od dołu do góry (co odbiega od reguły wyrażenia regularnego), a w drugiej fazie renderujemy od góry do dołu. W obu fazach mogliśmy używać istniejących dopasowywaczy i renderowania opartych na wyrażeniach regularnych, praktycznie bez zmian, dzięki czemu mogliśmy je migrować pojedynczo.

Etap 1. Dopasowywanie od dołu do góry

Pierwszy etap mniej więcej dokładnie i wyłącznie wykonuje to, co jest napisane na okładce. Przechodzimy po drzewie od dołu do góry i próbujemy dopasować wyrażenia podrzędne w każdym odwiedzanym węźle drzewa składni. Aby dopasować określony podwyraz, funkcja dopasowywania może używać wyrażenia regularnego tak samo jak w dotychczasowym systemie. W wersji 128 nadal tak się dzieje w niektórych przypadkach, np. gdy chodzi o dopasowanie długości. Zamiast tego może analizować strukturę poddrzewa z korzenia w bieżącym węźle. Dzięki temu może on wykrywać błędy składni i jednocześnie rejestrować informacje strukturalne.

Rozważ przykład drzewa składni z powyższego opisu:

Etap 1. Dopasowywanie od dołu w drzewie składni.

W tym przypadku nasze dopasowywacze byłyby stosowane w tej kolejności:

  1. hsl(177degvar(--saturation, 100%) 50%): najpierw znajdujemy pierwszy argument wywołania funkcji hsl, czyli kąt odcienia barwy. Dopasowujemy go za pomocą funkcji dopasowywania kąta, aby można było ozdobić wartość kąta ikoną kąta.
  2. hsl(177degvar(--saturation, 100%)50%): po drugie, wykryliśmy wywołanie funkcji var za pomocą funkcji dopasowywania zmiennych. W przypadku takich połączeń chcemy przede wszystkim:
    • Odszukaj deklarację zmiennej i oblicz jej wartość, a potem dodaj do nazwy zmiennej link i wyskakujące okienko, aby się z nimi połączyć.
    • Udekoruj wywołanie kolorową ikoną, jeśli obliczona wartość jest kolorem. Jest jeszcze trzecia rzecz, ale o niej porozmawiamy później.
  3. hsl(177deg var(--saturation, 100%) 50%): na koniec dopasowujemy wyrażenie wywołania do funkcji hsl, aby można było ją ozdobić kolorową ikoną.

Oprócz wyszukiwania wyrażeń podrzędnych, które chcemy ozdobić, w ramach procesu dopasowywania stosujemy jeszcze jedną funkcję. Pamiętaj, że w kroku 2. mieliśmy zamiar sprawdzić obliczoną wartość nazwy zmiennej. W rzeczywistości idziemy o krok dalej i przekazujemy wyniki w dół drzewa. I nie tylko w przypadku zmiennej, ale też wartości zastępczej. Podczas odwiedzania węzła funkcji var jego podrzędne są już odwiedzone, więc znamy już wyniki wszystkich funkcji var, które mogą pojawić się w wartości zastępczej. Dzięki temu możemy łatwo i tanio zastępować funkcje var ich wynikami w bieżącym czasie, co pozwala nam w prosty sposób odpowiadać na pytania w rodzaju „Czy wynik tego wywołania funkcji var to kolor?”, tak jak w kroku 2.

Etap 2. Renderowanie od góry do dołu

W drugim etapie odwracamy kierunek. Korzystając z wyników dopasowania z etapy 1, renderujemy drzewo w formacie HTML, przechodząc przez nie od góry do dołu. W przypadku każdego odwiedzonego węzła sprawdzamy, czy jest on zgodny, a jeśli tak, wywołujemy odpowiedni dla niego moduł renderujący. Unikniemy konieczności specjalnego traktowania węzłów, które zawierają tylko tekst (takich jak NumberLiteral „50%”), ponieważ dołączamy domyślny element dopasowujący i renderujący dla węzłów tekstowych. W ramach tego procesu renderowanie polega na wyprowadzaniu węzłów HTML, które po połączeniu tworzą reprezentację wartości właściwości wraz z ozdobnikami.

Faza 2. Interpretacja od góry do dołu drzewa składni.

W przypadku drzewa przykładowego wartość właściwości jest renderowana w tej kolejności:

  1. Zapoznaj się z wywołaniem funkcji hsl. Dopasowanie się udało, więc wywołaj funkcję renderowania kolorów. Ma on 2 funkcje:
    • Oblicza rzeczywistą wartość koloru, używając mechanizmu zastępowania na bieżąco w przypadku dowolnych argumentów var, a następnie rysuje ikonę koloru.
    • Rekursywnie renderuje elementy podrzędne elementu CallExpression. Automatycznie renderuje nazwę funkcji, nawiasy i przecinki, które są tylko tekstem.
  2. Otwórz pierwszy argument wywołania hsl. Jeśli się zgadza, wywołaj moduł renderowania kąta, który rysuje ikonę kąta i tekst kąta.
  3. Przejdź do drugiego argumentu, którym jest wywołanie funkcji var. Pasuje, więc wywołaj zmienną renderer, która zwróci:
    • Tekst var( na początku.
    • nazwę zmiennej i ozdobia ją linkiem do jej definicji lub szarym kolorem tekstu, aby wskazać, że nie została zdefiniowana. Dodaje też do niej wyskakujące okienko z informacjami o jej wartości.
    • Następnie, po przecinku, wartość zastępcza jest renderowana rekurencyjnie.
    • Zamknięcie nawiasu.
  4. Odwiedź ostatni argument wywołania hsl. Nie pasuje, więc wyświetl tylko zawartość tekstową.

Czy zauważyłeś/zauważyłaś, że w tym algorytmie renderowanie w pełni kontroluje sposób renderowania elementów podrzędnych dopasowanego węzła? Rekursywne renderowanie podrzędnych jest działaniem zapobiegawczym. Umożliwiło to stopniową migrację z renderowania opartego na wyrażeniach regularnych na renderowanie oparte na drzewie składni. W przypadku węzłów dopasowanych za pomocą starszego dopasowania wyrażenia regularnego można użyć odpowiadającego mu renderowania w pierwotnej formie. W języku drzewa składniowego odpowiada ono za renderowanie całego poddrzewa, a jego wynik (węzeł HTML) może być płynnie wstawiany w otaczający proces renderowania. Dzięki temu mogliśmy przenosić parowniki i renderowanie w parach oraz wymieniać je pojedynczo.

Kolejną przydatną funkcją renderowania jest możliwość kontrolowania renderowania elementów potomnych dopasowanego węzła, co pozwala nam uwzględniać zależności między dodawanymi ikonami. W przypadku przykładu powyżej kolor generowany przez funkcję hsl zależy oczywiście od wartości odcienia. Oznacza to, że kolor ikony koloru zależy od kąta pokazanego przez ikonę kąta. Jeśli użytkownik otworzy edytor kąta za pomocą tej ikony i zmodyfikuje kąt, będziemy mogli zaktualizować kolor ikony w czasie rzeczywistym:

Jak widać w powyższym przykładzie, używamy tego mechanizmu również w przypadku innych par ikon, np. color-mix() i jego 2 kanałów kolorów lub funkcji var, które zwracają kolor z opcji zastępczej.

Wpływ na wydajność

Gdy zaczęliśmy zajmować się tym problemem, aby zwiększyć niezawodność i rozwiązać od dawna występujące problemy, spodziewaliśmy się pewnego spadku wydajności, ponieważ zaczęliśmy używać pełnego parsowania. Aby przetestować tę funkcję, utworzyliśmy benchmark, który renderuje około 3,5 tys. deklaracji właściwości. Na maszynie M1 przeprofilowaliśmy wersje oparte na wyrażeniach regularnych i analizatorze z 6-krotnym ograniczeniem przepustowości.

Zgodnie z oczekiwaniami okazało się, że w tym przypadku podejście oparte na parsowaniu było o 27% wolniejsze od podejścia opartego na wyrażeniach regularnych. Wyświetlanie za pomocą podejścia opartego na wyrażeniu regularnym zajęło 11 s, a wyświetlanie za pomocą podejścia opartego na parsowaniu – 15 s.

Biorąc pod uwagę korzyści płynące z nowego podejścia, zdecydowaliśmy się na jego wdrożenie.

Podziękowania

Dziękujemy Sofii Emelianova i Jecelyn Yeen za nieocenioną pomoc w edytowaniu tego posta.

Pobieranie kanałów podglądu

Rozważ użycie jako domyślnej przeglądarki deweloperskiej wersji Canary, Dev lub Beta przeglądarki Chrome. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.