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 są ostatnio nieco bardziej dopracowane? 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ą:

U góry: jest najnowsza wersja Chrome, na dole: Chrome 121.

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

  • color-mix. Przydatny podgląd obrazujący 2 argumenty koloru w 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 jeśli rozwiązania okażą się skuteczne, ich prawidłowe działanie często wymagało wiele wysiłku.

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 ewolucją, system wymagał pełnego zmodyfikowania. To 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 deklaracja właściwości jest badana w celu identyfikacji powiązanych z nimi komponentów i relacji. Na przykład deklaracja border: 1px solid red rozpoznałaby 1px jako długość, solid jako ciąg znaków i red jako kolor.
  2. Renderowanie. Na podstawie analizy strukturalnej w fazie renderowania komponenty są przekształcane w postać HTML. Dzięki temu wyświetlany tekst zawiera interaktywne elementy i wskazówki wizualne. Na przykład wartość koloru red jest renderowana z klikalną ikoną koloru, która po kliknięciu wyświetla selektor kolorów, który można łatwo zmienić.

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. Na przykład były 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.

Sprawdzało się to w większości przypadków, ale liczba przypadków nie rosła. Z lat uzyskaliśmy sporo raportów o błędach, w których dopasowanie nie było prawidłowe. Gdy je naprawialiśmy – niektóre proste poprawki, inne – dość zaawansowane – musieliśmy zmienić nasze podejście, aby pozbyć się długu technologicznego. Przyjrzyjmy się niektórym z nich.

Dopasowanie: color-mix()

Wyrażenie regularne użyte w funkcji color-mix() było następujące:

/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ły, dlatego załączyliśmy specjalną obsługę do obsługi bardziej złożonych argumentów funkcji, takich jak funkcje var. Jak widać na pierwszym zrzucie ekranu, nie zawsze to działa.

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żyte do dopasowywania kolorów zawierało wyrażenie podrzędne \b[a-zA-Z]+\b(?!-) umożliwiające dopasowanie nazwanych kolorów, np. 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. Dlatego 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 przy użyciu wyrażeń regularnych przestanie działać (ponieważ analizowany język nie jest standardowy), można wybrać następny krok kanoniczny: użyć parsera gramatyki wyższego typu. W przypadku CSS oznacza to parser języków bez kontekstu. Taki system parsowania istniał już w kodzie źródłowym DevTools: Lezer CodeMirror, który stanowi podstawę np. podświetlania składni w CodeMirror, edytorze w panelu Źródła. Parser CSS Lezera umożliwiał nam tworzenie (nieabstrakcyjnych) drzew 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.

Poza tym okazało się, że bezpośrednie przejście z dopasowania opartego na wyrażeniach na dopasowywanie oparte na parserze jest niemożliwe: oba podejścia 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.

Opracowaliśmy więc rozwiązanie, które umożliwiło nam stopniowe wprowadzanie zmian, które opisujemy bardziej szczegółowo poniżej. Krótko mówiąc, zachowaliśmy podejście dwufazowe, ale w pierwszej fazie staramy się dopasować wyrażenia podrzędne od dołu do góry (co oznacza odejście od przepływu wyrażeń regularnych), a w drugiej fazie renderujemy od góry. 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 węźle drzewa składni, który odwiedzamy. Aby dopasować konkretne wyrażenie podrzędne, funkcja ta może użyć wyrażenia regularnego tak samo jak w dotychczasowym systemie. Od wersji 128 nadal robimy to w kilku przypadkach, na przykład w przypadku pasujących 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. Łączymy go z dopasowaniem kąta, dzięki czemu możemy udekorować 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, którą omówimy 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. Nie tylko w przypadku zmiennej, ale też wartości zastępczej. Gwarantujemy, że podczas odwiedzania węzła funkcji var jego elementy podrzędne zostały wcześniej 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 drugiej fazie robimy to w odwrotnym kierunku. 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. Unikamy specjalnej obsługi węzłów zawierających tylko tekst (np. NumberLiteral „50%”), udostępniając w ich przypadku domyślny mechanizm dopasowywania i mechanizmu renderowania. W ramach tego procesu renderowanie polega na generowaniu 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. Otwórz wywołanie funkcji hsl. Dopasowanie się udało, więc wywołaj moduł renderowania funkcji koloru. 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 to 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óry jest wywołaniem 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 znak przecinka rekursywnie renderuje wartość zastępczą.
    • Zamknięcie nawiasu.
  4. Odwiedź ostatni argument wywołania hsl. Nie było dopasowania, 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 na podstawie wyrażenia regularnego na renderowanie na podstawie drzewa 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 przypadku drzewa składniowego odpowiadałoby to za wyrenderowanie całego drzewa podrzędnego, a jego wynik (węzeł HTML) zostałby dobrze podłączony do otaczającego procesu renderowania. Dzięki temu mogliśmy przenosić parowniki i renderowanie w parach oraz wymieniać je pojedynczo.

Kolejną przydatną funkcją renderowania elementów potomnych dopasowanego węzła jest możliwość uwzględniania zależności między dodawanymi ikonami. W przykładzie powyżej kolor wygenerowany przez funkcję hsl oczywiście zależy od wartości barwy. 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.

Tak jak się spodziewaliśmy, metoda oparta na analizie okazała się w tym przypadku wolniejsza o 27% od metody opartej na wyrażeniach regularnych. Renderowanie oparte na wyrażeniach regularnych potrzebowało 11 sekund, a renderowanie oparte na parserze wymagało 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 przeglądarki Chrome Canary, Dev lub Beta jako domyślnej przeglądarki deweloperskiej. 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

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