מעבר לביטויים רגולריים: שיפור ניתוח ערכי ה-CSS בכלי הפיתוח ל-Chrome

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

שמתם לב שבזמן האחרון מאפייני ה-CSS בכרטיסייה Styles (סגנונות) בכלי הפיתוח ל-Chrome נראים קצת יותר מלוטשים? העדכונים האלה, שהושקו בין Chrome 121 ל-128, הם תוצאה של שיפור משמעותי באופן שבו אנחנו מנתחים ומציגים ערכים של CSS. במאמר הזה נסביר על הפרטים הטכניים של השינוי הזה – איך עוברים ממערכת התאמה של ביטויים רגולריים למנתח מתקדם יותר.

נבדוק את DevTools הנוכחי לעומת הגרסה הקודמת:

עליון: זה הגרסה העדכנית ביותר של Chrome, למטה: Chrome 121.

הבדל משמעותי, נכון? הנה פירוט של השיפורים העיקריים:

  • color-mix. תצוגה מקדימה נוחה שמציגה באופן חזותי את שני הארגומנטים של צבע בפונקציה color-mix.
  • pink. תצוגה מקדימה של הצבע שאפשר ללחוץ עליה עבור הצבע שנקרא pink. אפשר ללחוץ עליו כדי לפתוח בוחר צבעים ולשנות אותו בקלות.
  • var(--undefined, [fallback value]). טיפול משופר במשתנים לא מוגדרים, כאשר המשתנה הלא מוגדר מופיע באפור וערך החלופי הפעיל (במקרה הזה, צבע HSL) מוצג עם תצוגה מקדימה של צבע שאפשר ללחוץ עליה.
  • hsl(…): תצוגה מקדימה נוספת של צבע שניתן ללחוץ עליה עבור פונקציית הצבע hsl, שמספקת גישה מהירה לבורר הצבעים.
  • 177deg: שעון זווית שניתן ללחוץ עליו, שמאפשר לכם לגרור ולשנות את ערך הזווית באופן אינטראקטיבי.
  • var(--saturation, …): קישור שניתן ללחוץ עליו להגדרת הנכס בהתאמה אישית, שמאפשר לעבור בקלות להצהרה הרלוונטית.

ההבדל בולט. כדי לעשות זאת, נאלצנו ללמד את DevTools להבין ערכים של מאפייני CSS בצורה טובה יותר מבעבר.

האם התצוגות המקדימות האלה לא היו זמינות כבר?

סמלי התצוגה המקדימה האלה אולי נראים מוכרים, אבל הם לא תמיד הוצגו באופן עקבי, במיוחד בסינטקס CSS מורכב כמו בדוגמה שלמעלה. גם במקרים שבהם הם עבדו, בדרך כלל נדרשו מאמצים משמעותיים כדי לגרום להם לפעול כמו שצריך.

הסיבה לכך היא שהמערכת לניתוח ערכים התפתחה באופן אורגני מאז הימים הראשונים של DevTools. עם זאת, השירות לא הצליח לעמוד בקצב של התכונות החדשות המדהימות האחרונות שאנחנו מקבלים משירות CSS, והעלייה במורכבות השפה. המערכת דרשה עיצוב מחדש כדי לעמוד באבולוציה וזה בדיוק מה שעשינו!

איך מתבצע העיבוד של ערכי נכסי CSS

בכלי הפיתוח, תהליך היצירה וההוספה של עיטורים להצהרות על נכסים בכרטיסייה Styles מחולק לשני שלבים נפרדים:

  1. ניתוח מבני. בשלב הראשוני הזה מתבצעת ניתוח של הצהרת הנכס כדי לזהות את הרכיבים הבסיסיים שלו ואת היחסים ביניהם. לדוגמה, בהצהרה border: 1px solid red, המערכת תזהה את 1px כאורך, את solid כמחרוזת ואת red כצבע.
  2. עיבוד. בשלב העיבוד, על סמך הניתוח המבני, הרכיבים האלה הופכים לייצוג HTML. כך אפשר להעשיר את הטקסט של הנכס המוצג ברכיבים אינטראקטיביים וברמזים חזותיים. לדוגמה, ערך הצבע red מוצג באמצעות סמל צבע שניתן ללחוץ עליו. כשלוחצים עליו, מוצג בוחר צבעים שמאפשר לשנות את הצבע בקלות.

ביטויים רגולריים

בעבר הסתמכנו על ביטויים רגולריים (regex) כדי לנתח את ערכי הנכסים לניתוח מבני. הכנו רשימה של ביטויים רגולריים שמתאימים לקטעי הערכים של הנכסים שחשבנו עליהם. לדוגמה, היו ביטויים שתואמים לצבעים, לאורך, לזוויות של CSS, לביטויים משנה מורכבים יותר כמו קריאות לפונקציות var וכו'. סרקנו את הטקסט משמאל לימין כדי לבצע ניתוח ערכים, תוך חיפוש מתמיד של הביטוי הראשון ברשימה שתואם לחלק הבא בטקסט.

בדרך כלל הפעולה הזו עבדה כמו שצריך, אבל מספר המקרים שבהם התהליך לא גדל. במהלך השנים קיבלנו מספר רב של דוחות על באגים, כאשר ההתאמה לא בדיוק הצליחה. כשטיפלנו בהן – חלק מהתיקונים היו פשוטים, וחלקם היו מורכבים למדי – נאלצנו לחשוב מחדש על הגישה שלנו כדי לצמצם את החוב הטכני. בואו נבחן כמה מהבעיות!

התאמה ל-color-mix()

הביטוי הרגולרי שבו השתמשנו לפונקציה color-mix() היה כך:

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

התאמה לתחביר שלו:

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

אפשר לנסות להריץ את הדוגמה הבאה כדי להציג חזותית את ההתאמות.

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

תוצאת ההתאמה של פונקציית ערבוב הצבעים.

הדוגמה הפשוטה יותר עובדת כמו שצריך. עם זאת, בדוגמה המורכבת יותר, ההתאמה של <firstColor> היא hsl(177deg var(--saturation וההתאמה של <secondColor> היא 100%) 50%)), שפירושן לא ברור.

ידענו שזו בעיה. אחרי הכל, CSS כשפה פורמלית היא לא רגילה, ולכן כבר הוספנו טיפול מיוחד כדי לטפל בארגומנטים מורכבים יותר של פונקציות, כמו פונקציות var. עם זאת, כפי שאפשר לראות בצילום המסך הראשון, הפתרון הזה עדיין לא עבד בכל המקרים.

התאמה ל-tan()

אחד הבאגים הדיווחים הכי מצחיקים היה לגבי הפונקציה tan() הטריגונומטרית . ביטוי ה-regex שבו השתמשנו להתאמת צבעים כלל ביטוי משנה \b[a-zA-Z]+\b(?!-) להתאמה לצבעים עם שם, כמו מילת המפתח red. לאחר מכן בדקנו אם החלק התואם הוא למעשה צבע בעל שם, ותאמינו או לא, גם tan הוא צבע בעל שם! לכן, פירשנו בטעות ביטויים של tan() כצבעים.

התאמה ל-var()

נבחן דוגמה נוספת: פונקציות var() עם חלופה שמכילה הפניות אחרות ל-var(): var(--non-existent, var(--margin-vertical)).

נשמח לקבל התאמה לביטוי הרגולרי שלנו עבור var(). מלבד זאת, היא תפסיק את ההתאמה בסוגריים הסוגרים הראשונים. לכן הטקסט שלמעלה תואם כ-var(--non-existent, var(--margin-vertical). זוהי אחת מהמגבלות הידועות של התאמה לביטוי רגולרי. שפות שדורשות סוגריים תואמים הן לא רגולריות באופן מהותי.

מעבר למנתח CSS

כשניתוח טקסט באמצעות ביטויים רגולריים מפסיק לפעול (כי השפה שנותחת היא לא רגולרית), יש שלב קנוני הבא: שימוש בניתוח תחביר של סוג גבוה יותר. ב-CSS, המשמעות היא שימוש בניתוח תחביר של שפות ללא הקשר. למעשה, מערכת ניתוח כזו כבר הייתה קיימת ב-codebase של DevTools: Lezer של CodeMirror, שהוא הבסיס להדגשת תחביר ב-CodeMirror, של העורך שמופיע בחלונית מקורות. מנתח ה-CSS של Lezer אפשר לנו ליצור עצי תחביר (לא מופשטים) של כללי CSS, והוא היה מוכן לשימוש. ניצחון.

עץ תחביר לערך המאפיין `hsl(177deg var(--saturation, 100%) 50%)`. זוהי גרסה פשוטה יותר של התוצאה שהופקה על ידי מנתח Lezer, ללא צמתים תחביריים לחלוטין עבור פסיקים וסוגריים.

עם זאת, גילינו שלא ניתן לעבור ישירות מהתאמה שמבוססת על ביטוי רגולרי להתאמה שמבוססת על מנתח: שתי הגישות פועלות בכיוונים מנוגדים. כשמתאימים בין קטעי ערכים לביטויים רגולריים, כלי הפיתוח סורקים את הקלט משמאל לימין, ומנסים שוב ושוב למצוא את ההתאמה המוקדמת ביותר מתוך רשימה ממוינת של תבניות. בעץ תחביר, ההתאמה תתחיל מלמטה למעלה. לדוגמה, תחילה יתבצע ניתוח של הארגומנטים של קריאה, לפני שתתבצע ניסיון להתאים את קריאת הפונקציה. אפשר לחשוב על זה כעל הערכה של ביטוי אריתמטי, שבו קודם מתייחסים לביטויים בסוגריים, אחר כך למפעילים מכפילים ואז למפעילים נוספים. במסגרת הזו, ההתאמה שמבוססת על ביטוי רגולרי תואמת להערכת הביטוי האריתמטי מימין לשמאל. לא ממש רצינו לכתוב מחדש את כל מערכת ההתאמה מאפס: היו 15 צמדי מתאמים ומעבדנים שונים, עם אלפי שורות קוד, ולכן לא סביר שנוכל להשיק את המערכת באירוע ציון דרך אחד.

לכן חשבנו על פתרון שמאפשר לנו לבצע שינויים מצטברים, שנתאר אותם בפירוט בהמשך. בקצרה, שמרנו על הגישה הדו-שלבית, אבל בשלב הראשון אנחנו מנסים להתאים ביטויי משנה מלמטה למעלה (וכך המערכת מקוטעת את הרצף של הביטויים הרגולריים), ובשלב השני אנחנו מעבדים את התוכן מלמעלה למטה. בשני השלבים, יכולנו להשתמש בהתאמות ובעיבודים הקיימים שמבוססים על ביטויים רגולריים (regex), כמעט ללא שינוי, ולכן הצלחנו להעביר אותם אחד אחרי השני.

שלב 1: התאמה מלמטה למעלה

השלב הראשון הוא, פחות או יותר, בדיוק מה שמופיע בעטיפה של הסרטון. אנחנו חוצים את העץ לפי הסדר מלמטה עד למעלה ומנסים להתאים ביטויי משנה בכל צומת עץ תחביר שבו אנחנו מבקרים. כדי לבצע התאמה לתת-ביטוי ספציפי, כלי ההתאמה יכול להשתמש בביטוי רגולרי (regex) בדיוק כמו במערכת הקיימת. החל מגרסה 128, אנחנו עדיין עושים זאת במספר מקרים, לדוגמה, להתאמות באורכים. לחלופין, כלי תואם יכול לנתח את המבנה של עץ המשנה שעבר תהליך רוט (Root) בצומת הנוכחי. כך הוא יכול לזהות שגיאות תחביר ולתעד מידע מבני בו-זמנית.

בדוגמה של עץ התחביר שלמעלה:

שלב 1: התאמה מלמטה למעלה בעץ התחביר.

בעץ הזה, ה-matchers שלנו יחולו בסדר הבא:

  1. hsl(177degvar(--saturation, 100%) 50%): קודם כול, אנחנו מגלים את הארגומנט הראשון של קריאת הפונקציה hsl, זווית הגוון. אנחנו מתאימים אותו למתאמת זוויות, כדי שנוכל לקשט את ערך הזווית בסמל הזווית.
  2. hsl(177degvar(--saturation, 100%)50%): בשלב השני, אנחנו מגלים את קריאת הפונקציה var באמצעות מתאם var. בקריאות כאלה אנחנו רוצים בעיקר לעשות שני דברים:
    • מחפשים את ההצהרה על המשתנה ומחשבים את הערך שלו, ומוסיפים קישור וחלון קופץ לשם המשתנה כדי לקשר אליהם, בהתאמה.
    • אם הערך המחושב הוא צבע, אפשר לקשט את הקריאה בסמל צבע. למעשה, יש עוד דבר שלישי, אבל נדבר עליו בהמשך.
  3. hsl(177deg var(--saturation, 100%) 50%): לבסוף, אנחנו מתאימים את ביטוי הקריאה לפונקציה hsl כדי שנוכל לקשט אותו בסמל הצבע.

בנוסף לחיפוש ביטויי משנה שאנחנו רוצים לקשט, יש למעשה תכונה נוספת שאנחנו מפעילים כחלק מתהליך ההתאמה. שימו לב שבשלב 2, אמרנו שאנחנו מחפשים את הערך המחושב עבור שם משתנה. למעשה, אנחנו עוברים צעד אחד קדימה ומפיצים את התוצאות למעלה בעץ. ולא רק עבור המשתנה, אלא גם עבור ערך החלופי! מובטח שבזמן ביקור בצומת של פונקציה var, הצאצאים שלו כבר ביקרו מראש, כך שאנחנו כבר יודעים מה התוצאות של כל פונקציות var שעשויות להופיע בערך החלופי. לכן אנחנו יכולים להחליף פונקציות var בתוצאות שלהן בזמן אמת בקלות ובזול, וכך לענות בקלות על שאלות כמו "האם התוצאה של קריאת var הזו היא צבע?", כפי שעשינו בשלב 2.

שלב 2: רינדור מלמעלה למטה

בשלב השני, אנחנו הופכים את הכיוון. על סמך תוצאות ההתאמה משלב 1, אנחנו מייצרים את העץ ב-HTML על ידי סריקה שלו לפי הסדר מלמעלה למטה. לכל צומת שנבקר, אנחנו בודקים אם הוא תואם, ואם כן, קוראים למעבד התצוגה התואם של המתאמים. כדי להימנע מהצורך בטיפול מיוחד בצמתים שמכילים רק טקסט (כמו NumberLiteral '50%'), אנחנו כוללים מתאים ומעבד ברירת מחדל לצמתי טקסט. הרסטוררים פשוט מניבים צמתים של HTML, שכשמשלבים אותם יוצרים את הייצוג של ערך הנכס, כולל הקישוט שלו.

שלב 2: עיבוד מלמעלה למטה בעץ התחביר.

בעץ לדוגמה, זהו הסדר שבו ערך המאפיין מוצג:

  1. פונקציית הקריאה hsl. הייתה התאמה, לכן צריך לקרוא למעבד של פונקציית הצבע. הוא עושה שתי דברים:
    • הפונקציה מחשבת את ערך הצבע בפועל באמצעות מנגנון החלפה בזמן אמת לכל ארגומנטים של var, ולאחר מכן מצייר סמל צבע.
    • עיבוד רספונסיבי רפלקסיבי של הצאצאים של CallExpression. כך יתבצע באופן אוטומטי עיבוד של שם הפונקציה, הסוגריים והפסיקים, שהם רק טקסט.
  2. נכנסים לארגומנט הראשון של קריאת hsl. הוא התאים, לכן צריך להפעיל את ה-angle renderer, שמצייר את סמל הזווית ואת הטקסט של הזווית.
  3. נכנסים לארגומנט השני, שהוא הקריאה ל-var. התוצאה תואמת, לכן צריך להפעיל את המשתנה renderer, שמציג את הפלט הבא:
    • הטקסט var( בהתחלה.
    • שם המשתנה, ומוסיף לו קישור להגדרת המשתנה או צבע טקסט אפור כדי לציין שהוא לא הוגדר. בנוסף, הוא מוסיף למשתנה חלון קופץ כדי להציג מידע על הערך שלו.
    • הנקודה הפסיק ואז ערך ברירת המחדל מוצגים באופן רפליקטיבי.
    • סוגר סוגריים.
  4. כניסה לארגומנט האחרון של הקריאה hsl. לא הייתה התאמה, ולכן צריך להפיק רק את תוכן הטקסט שלו.

שמתם לב שבאלגוריתם הזה, ה-render קובע באופן מלא איך הילדים של צומת תואם יוצגו? עיבוד חזותי רפלוקטיבי של הצאצאים הוא יזום. הטריק הזה אפשר העברה מדורגת מעיבוד שמבוסס על ביטוי רגולרי לעיבוד שמבוסס על עץ תחביר. בצמתים שתואמים למתאמי ביטוי רגולריים מדור קודם, אפשר להשתמש במעבד התצוגה התואם בצורתו המקורית. במונחים של עץ תחביר, הוא יישא באחריות לעיבוד הגרף המשני כולו, והתוצאה שלו (צומת HTML) תוכל להשתלב בצורה חלקה בתהליך העיבוד שמקיף אותו. כך יכולנו לאפשר לשקע למצוא התאמות ומעבדים בזוגות, ולהחליף אותם אחד אחרי השני.

תכונה מגניבה נוספת של מנועי עיבוד תמונה ששולטים בעיבוד התמונות של הצאצאים של הצומת התואם היא שהם מאפשרים לנו להסיק מסקנות לגבי יחסי התלות בין הסמלים שאנחנו מוסיפים. בדוגמה שלמעלה, ברור שהצבע שנוצר על ידי הפונקציה hsl תלוי בערך הטון שלו. כלומר, הצבע שמוצג בסמל הצבע תלוי בזווית שמוצגת בסמל הזווית. אם המשתמש פותח את עורך הזווית דרך הסמל הזה ומשנה את הזווית, אנחנו יכולים עכשיו לעדכן את הצבע של סמל הצבע בזמן אמת:

כפי שאפשר לראות בדוגמה שלמעלה, אנחנו משתמשים במנגנון הזה גם בשילובים אחרים של סמלים, כמו color-mix() ושני ערוצי הצבעים שלו, או פונקציות var שמחזירות צבע מהחלופה שלה.

השפעה על הביצועים

כשהתחלנו לטפל בבעיה הזו כדי לשפר את האמינות ולתקן בעיות קיימות, ציפינו לירידה מסוימת בביצועים, כי התחלנו להריץ מנתח מלא. כדי לבדוק את זה, יצרנו מדד השוואה שמייצר כ-3,500 הצהרות על נכסים, וביצענו פרופיל גם לגרסה המבוססת על ביטוי רגולרי וגם לגרסה המבוססת על מנתח, עם 6x ניתוב תעבורה (throttling) במכונה עם מעבד M1.

כצפוי, הגישה שמבוססת על ניתוח הייתה איטית ב-27% מהגישה שמבוססת על ביטוי רגולרי במקרה הזה. במסגרת הגישה שמבוססת על ביטוי רגולרי (regex) נמשכה 11 שניות כדי לעבד את המידע, ולגישה המבוססת על מנתח נתונים נמשכה 15 שניות.

בהתחשב בניצחונות שאנחנו מקבלים מהגישה החדשה, החלטנו להמשיך בתהליך.

אישורים

אנחנו רוצים להודות מקרב לב ל-Sofia Emelianova ול-Jecelyn Yeen על העזרה החשובה בעריכת הפוסט הזה.

הורדת הערוצים לתצוגה מקדימה

מומלץ להשתמש ב-Chrome Canary, ב-Dev או ב-Beta כדפדפן הפיתוח שמוגדר כברירת מחדל. ערוצי התצוגה המקדימה האלה מעניקים לכם גישה לתכונות העדכניות ביותר של DevTools, מאפשרים לכם לבדוק ממשקי API מתקדמים לפלטפורמות אינטרנט ולמצוא בעיות באתר לפני שהמשתמשים שלכם יעשו זאת.

פנייה לצוות של כלי הפיתוח ל-Chrome

אתם יכולים להשתמש באפשרויות הבאות כדי לדון בתכונות החדשות, בעדכונים או בכל דבר אחר שקשור ל-DevTools.