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żyliście, że właściwości CSS na karcie Style w Narzędziach deweloperskich w Chrome wyglądają ostatnio nieco lepiej? Te aktualizacje, wprowadzone w Chrome 121 i 128, są wynikiem znacznego ulepszenia sposobu analizowania i prezentowania wartości CSS. W tym artykule omówimy szczegóły techniczne tej zmiany – przejścia z systemu dopasowywania wyrażeń regularnych na bardziej niezawodny parser.

Porównajmy bieżącą wersję Narzędzi deweloperskich z poprzednią:

U góry: najnowsza wersja Chrome, u dołu: Chrome 121.

To całkiem sporo, prawda? Oto podsumowanie najważniejszych ulepszeń:

  • color-mix. Przydatny podgląd, który wizualnie przedstawia 2 argumenty koloru w funkcji color-mix.
  • pink. Klikalny podgląd koloru dla nazwanego koloru pink. Kliknij go, aby otworzyć selektor kolorów i łatwo dostosować kolor.
  • var(--undefined, [fallback value]). Ulepszona obsługa niezdefiniowanych zmiennych, przy czym niezdefiniowana zmienna jest wyszarzona, a aktywna wartość zastępcza (w tym przypadku kolor HSL) jest wyświetlana z klikalnym podglądem koloru.
  • hsl(…): kolejny klikalny podgląd koloru dla funkcji koloru hsl, który 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ć Narzędzia deweloperskie znacznie lepiej rozumieć wartości właściwości CSS.

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 w sposób spójny, zwłaszcza w przypadku złożonej składni CSS, jak w przykładzie powyżej. Nawet jeśli działały, często wymagały znacznego wysiłku, aby funkcjonowały prawidłowo.

Wynika to z tego, że system analizowania wartości rozwijał się organicznie od pierwszych dni Narzędzi deweloperskich. Nie nadąża jednak za najnowszymi, niesamowitymi funkcjami CSS i odpowiednim wzrostem złożoności języka. System wymagał całkowitego przeprojektowania, aby nadążyć za rozwojem, i właśnie to zrobiliśmy.

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

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

  1. Analiza strukturalna. W tej początkowej fazie deklaracja właściwości jest analizowana pod kątem jej podstawowych komponentów i ich relacji. Na przykład w deklaracji border: 1px solid red rozpozna 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 reprezentację HTML. Dzięki temu wyświetlany tekst dotyczący nieruchomości jest wzbogacony o elementy interaktywne i wskazówki wizualne. Na przykład wartość koloru red jest renderowana z klikniętą ikoną koloru, która po kliknięciu wyświetla selektor kolorów ułatwiający 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 dekorowanie. Były to na przykład wyrażenia pasujące do kolorów, długości i kątów CSS, bardziej skomplikowane podwyrażenia, takie jak wywołania funkcji var, i tak dalej. Aby przeprowadzić analizę wartości, przeskanowaliśmy tekst od lewej do prawej, stale szukając pierwszego wyrażenia z listy, które pasuje do następnego fragmentu tekstu.

W większości przypadków to wystarczyło, ale liczba sytuacji, w których to nie działało, stale rosła. Przez lata otrzymaliśmy wiele zgłoszeń błędów, w których dopasowanie nie było do końca prawidłowe. Gdy je naprawialiśmy – niektóre poprawki były proste, inne dość skomplikowane – musieliśmy przemyśleć nasze podejście, aby utrzymać dług techniczny w ryzach. Przyjrzyjmy się niektórym problemom.

Pasuje do: color-mix()

Wyrażenie regularne, którego użyliśmy w funkcji color-mix(), było takie:

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

co odpowiada składni:

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

Aby zwizualizować dopasowania, uruchom ten 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 dla funkcji color-mix.

Prostszy przykład działa prawidłowo. Jednak w bardziej złożonym przykładzie dopasowanie <firstColor> to hsl(177deg var(--saturation, a dopasowanie <secondColor> to 100%) 50%)), co jest zupełnie bez sensu.

Wiedzieliśmy, że to problem. W końcu CSS jako język formalny nie jest regularny, więc uwzględniliśmy już specjalną obsługę bardziej skomplikowanych argumentów funkcji, takich jak funkcje var. Jednak jak widać na pierwszym zrzucie ekranu, nie zawsze to działało.

Pasuje do: tan()

Jednym z bardziej zabawnych zgłoszonych błędów był ten dotyczący funkcji trygonometrycznej tan(). Wyrażenie regularne, którego używaliśmy do dopasowywania kolorów, zawierało podwyrażenie \b[a-zA-Z]+\b(?!-) do dopasowywania nazwanych kolorów, takich jak słowo kluczowe red. Następnie sprawdziliśmy, czy dopasowana część jest rzeczywiście nazwanym kolorem i okazało się, że tan też jest nazwanym kolorem. Dlatego błędnie zinterpretowaliśmy wyrażenia tan() jako kolory.

Pasuje do: var()

Spójrzmy na inny przykład: funkcje var() z wartością zastępczą zawierającą inne odwołania do var(): var(--non-existent, var(--margin-vertical)).

Nasze wyrażenie regularne dla var() pasowałoby do tej wartości. Z tym że dopasowanie zatrzymałoby się na pierwszym nawiasie zamykającym. Powyższy tekst zostanie więc dopasowany jako var(--non-existent, var(--margin-vertical). To typowe ograniczenie dopasowywania wyrażeń regularnych. Języki, które wymagają dopasowania nawiasów, nie są zasadniczo regularne.

Przejście na parser CSS

Gdy analiza tekstu za pomocą wyrażeń regularnych przestaje działać (ponieważ analizowany język nie jest regularny), kolejnym krokiem jest użycie parsera dla gramatyki wyższego typu. W przypadku CSS oznacza to parser dla języków bezkontekstowych. W rzeczywistości taki system parsera istniał już w bazie kodu Narzędzi deweloperskich: Lezer CodeMirror, który jest podstawą np. wyróżniania składni w CodeMirror, edytorze dostępnym w panelu Źródła. Parser CSS Lezera pozwolił nam wygenerować (nieabstrakcyjne) drzewa składniowe dla reguł CSS i był gotowy do użycia. Zwycięstwo.

Drzewo składniowe dla wartości właściwości `hsl(177deg var(--saturation, 100%) 50%)`. Jest to uproszczona wersja wyniku wygenerowanego przez parser Lezer, która nie zawiera węzłów czysto składniowych dla przecinków i nawiasów.

Okazało się jednak, że nie da się bezpośrednio przejść z dopasowywania opartego na wyrażeniach regularnych na dopasowywanie oparte na parserze, ponieważ te 2 podejścia działają w przeciwnych kierunkach. Podczas dopasowywania fragmentów wartości za pomocą wyrażeń regularnych Narzędzia deweloperskie skanowały dane wejściowe od lewej do prawej, wielokrotnie próbując znaleźć najwcześniejsze dopasowanie z uporządkowanej listy wzorców. W przypadku drzewa składniowego dopasowywanie rozpoczyna się od dołu do góry, na przykład najpierw analizowane są argumenty wywołania, a dopiero potem następuje próba dopasowania wywołania funkcji. Można to porównać do obliczania wyrażenia arytmetycznego, w którym najpierw bierze się pod uwagę 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 przepisywać całego systemu dopasowywania od zera: było 15 różnych par dopasowywania i renderowania z tysiącami wierszy kodu, co sprawiało, że nie mogliśmy dostarczyć go w jednym etapie.

Dlatego opracowaliśmy rozwiązanie, które pozwoliło nam wprowadzać zmiany stopniowo. Poniżej opisujemy je bardziej szczegółowo. Krótko mówiąc, zachowaliśmy podejście dwuetapowe, ale w pierwszym etapie próbujemy dopasować podwyrażenia od dołu do góry (co jest niezgodne z przepływem wyrażeń regularnych), a w drugim etapie renderujemy od góry do dołu. W obu fazach mogliśmy używać istniejących dopasowań i renderów opartych na wyrażeniach regularnych, praktycznie bez zmian, dzięki czemu mogliśmy je przenosić pojedynczo.

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

Pierwsza faza polega mniej więcej na tym, co sugeruje jej nazwa. Przechodzimy przez drzewo w kolejności od dołu do góry i staramy się dopasować podwyrażenia w każdym odwiedzanym węźle drzewa składniowego. Aby dopasować konkretne podwyrażenie, dopasowanie może używać wyrażenia regularnego, tak jak w dotychczasowym systemie. W wersji 128 w niektórych przypadkach nadal tak jest, na przykład w przypadku dopasowywania długości. Może też analizować strukturę poddrzewa, którego korzeniem jest bieżący węzeł. Dzięki temu może jednocześnie wykrywać błędy składni i rejestrować informacje o strukturze.

Rozważmy podany wyżej przykład drzewa składniowego:

Etap 1. Dopasowywanie od dołu do góry w drzewie składniowym.

W przypadku tego drzewa dopasowania zostaną zastosowane w tej kolejności:

  1. hsl(177degvar(--saturation, 100%) 50%): najpierw odkrywamy pierwszy argument wywołania funkcji hsl, czyli kąt odcienia. Dopasowujemy go za pomocą dopasowania kąta, abyśmy mogli ozdobić wartość kąta ikoną kąta.
  2. hsl(177degvar(--saturation, 100%)50%): po drugie, wykrywamy wywołanie funkcji var za pomocą dopasowania zmiennej. W przypadku takich połączeń chcemy przede wszystkim zrobić 2 rzeczy:
    • Wyszukaj deklarację zmiennej i oblicz jej wartość, a następnie dodaj link i okienko do nazwy zmiennej, aby połączyć je odpowiednio z deklaracją i wartością.
    • Jeśli obliczona wartość jest kolorem, udekoruj wywołanie ikoną koloru. Jest jeszcze trzecia rzecz, ale o niej porozmawiamy później.
  3. hsl(177deg var(--saturation, 100%) 50%): Na koniec dopasowujemy wyrażenie wywołania funkcji hsl, abyśmy mogli ozdobić je ikoną koloru.

Oprócz wyszukiwania podwyrażeń, które chcemy udekorować, w ramach procesu dopasowywania uruchamiamy jeszcze jedną funkcję. Zwróć uwagę, że w kroku 2 wspomnieliśmy o wyszukiwaniu obliczonej wartości nazwy zmiennej. W rzeczywistości idziemy o krok dalej i propagujemy wyniki w górę drzewa. Dotyczy to nie tylko zmiennej, ale też wartości zastępczej. Gwarantujemy, że podczas odwiedzania węzła funkcji var jego elementy podrzędne zostały 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 na bieżąco, co pozwala nam łatwo odpowiadać na pytania takie jak „Czy wynik tego wywołania var jest kolorem?”, jak to zrobiliśmy w kroku 2.

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

W drugiej fazie zmieniamy kierunek. Biorąc wyniki dopasowania z etapu 1, renderujemy drzewo do kodu HTML, przechodząc przez nie w kolejności od góry do dołu. W przypadku każdego odwiedzonego węzła sprawdzamy, czy pasuje, a jeśli tak, wywołujemy odpowiedni moduł renderujący dopasowania. Unikamy konieczności specjalnego traktowania węzłów zawierających tylko tekst (np. NumberLiteral „50%”), dodając domyślny moduł dopasowujący i renderujący dla węzłów tekstowych. Moduły renderujące po prostu generują węzły HTML, które po połączeniu tworzą reprezentację wartości właściwości wraz z jej dekoracjami.

Faza 2. Renderowanie od góry do dołu w drzewie składni.

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

  1. Odwiedź wywołanie funkcji hsl. Pasuje, więc wywołaj funkcję renderującą kolor. Pozwala to osiągnąć 2 efekty:
    • Oblicza rzeczywistą wartość koloru, używając mechanizmu zastępowania na bieżąco dla wszystkich argumentów var, a następnie rysuje ikonę koloru.
    • Rekurencyjnie renderuje elementy podrzędne CallExpression. Automatycznie zajmuje się renderowaniem nazwy funkcji, nawiasów i przecinków, które są tylko tekstem.
  2. Odwiedź pierwszy argument wywołania hsl. Pasuje, więc 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 var. Warunek został spełniony, więc wywołaj zmienną renderer, która zwróci następujący wynik:
    • Tekst var( na początku.
    • nazwę zmiennej i oznacza ją linkiem do definicji zmiennej lub szarym kolorem tekstu, aby wskazać, że nie została zdefiniowana. Dodaje też do zmiennej wyskakujące okienko z informacjami o jej wartości.
    • przecinek, a następnie rekurencyjnie renderuje wartość zastępczą.
    • Nawias zamykający.
  4. Przejdź do ostatniego argumentu wywołania hsl. Nie pasuje, więc po prostu wyświetl jego treść tekstową.

Czy zauważyliście, że w tym algorytmie renderowanie w pełni kontroluje sposób renderowania elementów podrzędnych dopasowanego węzła? Rekurencyjne renderowanie elementów podrzędnych jest proaktywne. Ta sztuczka umożliwiła stopniową migrację z renderowania opartego na wyrażeniach regularnych do renderowania opartego na drzewie składniowym. W przypadku węzłów dopasowanych za pomocą starszego dopasowania wyrażeń regularnych odpowiedni mechanizm renderowania może być używany w pierwotnej formie. W terminologii drzewa składniowego odpowiadałby on za renderowanie całego poddrzewa, a jego wynik (węzeł HTML) można by było bez problemu podłączyć do otaczającego procesu renderowania. Dzięki temu mogliśmy przenosić dopasowania i renderery parami i wymieniać je pojedynczo.

Inną ciekawą funkcją modułów renderujących, które kontrolują renderowanie elementów podrzędnych dopasowanego węzła, jest możliwość wnioskowania o zależnościach między dodawanymi ikonami. W powyższym przykładzie kolor generowany przez funkcję hsl zależy oczywiście od wartości odcienia. Oznacza to, że kolor wyświetlany przez ikonę koloru zależy od kąta wyświetlanego przez ikonę kąta. Jeśli użytkownik otworzy edytor kąta za pomocą tej ikony i zmieni kąt, możemy teraz zaktualizować kolor ikony koloru w czasie rzeczywistym:

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

Wpływ na wydajność

Gdy zaczęliśmy analizować ten problem, aby poprawić niezawodność i rozwiązać długotrwałe problemy, spodziewaliśmy się pewnego pogorszenia wydajności, ponieważ zaczęliśmy używać w pełni funkcjonalnego parsera. Aby to przetestować, utworzyliśmy test porównawczy, który renderuje około 3,5 tys.deklaracji właściwości, i przeprowadziliśmy profilowanie wersji opartej na wyrażeniach regularnych i parserze z 6-krotnym ograniczaniem przepustowości na komputerze z procesorem M1.

Zgodnie z oczekiwaniami podejście oparte na analizie okazało się w tym przypadku o 27% wolniejsze niż podejście oparte na wyrażeniach regularnych. Wyrenderowanie strony przy użyciu metody opartej na wyrażeniach regularnych zajęło 11 sekund, a przy użyciu metody opartej na parserze – 15 sekund.

Biorąc pod uwagę korzyści, jakie daje nowe podejście, zdecydowaliśmy się je zastosować.

Podziękowania

Dziękujemy Sofii Emelianovej i Jecelyn Yeen za nieocenioną pomoc w redagowaniu tego posta.

Pobieranie kanałów podglądu

Rozważ użycie Chrome Canary, Dev lub Beta jako domyślnej przeglądarki do programowania. Te kanały wersji testowej dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znajdować problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Skorzystaj z poniższych opcji, aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi.