השוואת ביצועים של נכס CSS מסוג @property

תאריך פרסום: 2 באוקטובר 2024

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

השוואת ביצועים של CSS באמצעות PerfTestRunner

כדי לבדוק את הביצועים של CSS, פיתחנו את חבילת הבדיקות 'בדיקת ביצועים של סלקטורים ב-CSS'. הכלי מבוסס על PerfTestRunner של Chromium ומאפשר לבצע בדיקות ביצועים של CSS. PerfTestRunner הזה משמש את Blink – מנוע הרינדור הבסיסי של Chromium – לבדיקות הביצועים הפנימיות שלו.

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

const testResults = PerfTestRunner.measureRunsPerSecond({
  "Test Description",
  iterationCount: 5,
  bootstrap: function() {
    // Code to execute before all iterations run
    // For example, you can inject a style sheet here
  },
  setup: function() {
    // Code to execute before a single iteration
  },
  run: function() {
    // The actual test that gets run and measured.
    // A typical test adjusts something on the page causing a style or layout invalidation
  },
  tearDown: function() {
    // Code to execute after a single iteration has finished
    // For example, undo DOM adjustments made within run()
  },
  done: function() {
    // Code to be run after all iterations have finished.
    // For example, remove the style sheets that were injected in the bootstrap phase
  },
});

כל אפשרות ל-measureRunsPerSecond מתוארת באמצעות תגובות בבלוק הקוד, כאשר הפונקציה run היא החלק המרכזי שנמדד.

נקודות ההשוואה של הסלקטורים ב-CSS מחייבות עץ DOM

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

לדוגמה, הפונקציה makeTree הבאה היא חלק ממדדי הביצועים @property. הוא יוצר עץ של 1,000 רכיבים, וכל רכיב מכיל כמה רכיבי צאצא שמוטמעים בתוכו.

const $container = document.querySelector('#container');

function makeTree(parentEl, numSiblings) {
  for (var i = 0; i <= numSiblings; i++) {
    $container.appendChild(
      createElement('div', {
        className: `tagDiv wrap${i}`,
        innerHTML: `<div class="tagDiv layer1" data-div="layer1">
          <div class="tagDiv layer2">
            <ul class="tagUl">
              <li class="tagLi"><b class="tagB"><a href="/" class="tagA link" data-select="link">Select</a></b></li>
            </ul>
          </div>
        </div>`,
      })
    );
  }
}

makeTree($container, 1000);

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

הרצת נקודת השוואה

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

npm run start

אחרי שמתחילים, אפשר להיכנס למדד העזר בכתובת ה-URL שפורסמה שלו ולהריץ את window.startTest() באופן ידני.

כדי להריץ את מדדי הביצועים האלה בנפרד – ללא תוספים או גורמים אחרים – Puppeteer מופעל מ-CLI כדי לטעון ולהריץ את מדד הביצועים שהוענק.

כדי לבדוק את מדדי העמידה ביעדים האלה של @property באופן ספציפי, במקום להיכנס לדף הרלוונטי בכתובת ה-URL שלו http://localhost:3000/benchmarks/at-rule/at-property.html, מריצים את הפקודות הבאות ב-CLI:

npm run benchmark at-rule/at-property

הפעולה הזו טוען את הדף באמצעות Puppeteer, קוראת אוטומטית אל window.startTest() ומדווחת על התוצאות.

השוואת הביצועים של נכסי CSS

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

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

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

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

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

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

חשוב לבחור בקפידה את הנכסים להשוואה. מאפיינים מסוימים (כמו accent-color) מבטלים רק סגנונות, אבל יש מאפיינים רבים (כמו writing-mode) שמבטלים גם פעולות אחרות כמו פריסה או צבע. אתם רוצים את המאפיינים שמבטלים רק סגנונות.

כדי לקבוע זאת, אפשר לחפש ברשימת מאפייני ה-CSS של Blink. בכל נכס יש שדה invalidate שמציין אילו נכסים לא תקפים.

בנוסף, חשוב לבחור נכס מהרשימה שלא מסומן כ-independent, כי השוואת נכס כזה למדד תגרום לעיוות בתוצאות. לנכסים עצמאיים אין השפעות על נכסים או דגלים אחרים. כשרק נכסים עצמאיים משתנים, Blink משתמשת בנתיב קוד מהיר שמשכפל את הסגנון של הצאצא ומעדכנת את הערכים החדשים בעותק המשוכפל הזה. הגישה הזו מהירה יותר מביצוע חישוב מחדש מלא.

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

הקבוצה הראשונה של מדדי הביצועים מתמקדת במאפייני CSS שעוברים בירושה. יש שלושה סוגים של מאפיינים שעוברים בירושה לצורך בדיקה והשוואה ביניהם:

  • נכס רגיל שעובר בירושה: accent-color.
  • נכס מותאם אישית שאינו רשום: --unregistered.
  • מאפיין מותאם אישית שמירשם באמצעות inherits: true:‏ --registered.

נכסים מותאמים אישית לא רשומים מתווספים לרשימה הזו כי הם עוברים בירושה כברירת מחדל.

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

לגבי מאפיינים מותאמים אישית רשומים, רק אלה שבהם המתאר inherits מוגדר כ-True ייבדקו בהרצה הזו. המתאר inherits קובע אם הנכס יועבר בירושה לצאצאים או לא. לא משנה אם הנכס הזה רשום באמצעות CSS @property או JavaScript CSS.registerProperty, כי הרישום עצמו לא נכלל בבדיקה.

נקודות השוואה

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

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

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

לדוגמה, נקודת השוואה למדידת הביצועים של שינוי הסגנון של --registered נראית כך:

let i = 0;
PerfTestRunner.measureRunsPerSecond({
  description,
  iterationCount: 5,
  bootstrap: () => {
    setCSS(`@property --registered {
      syntax: "<number>";
      initial-value: 0;
      inherits: true;
    }`);
  },
  setup: function() {
    // NO-OP
  },
  run: function() {
    document.documentElement.style.setProperty('--registered', i);
    window.getComputedStyle(document.documentElement).getPropertyValue('--registered'); // Force style recalculation
    i = (i == 0) ? 1 : 0;
  },
  teardown: () => {
    document.documentElement.style.removeProperty('--registered');
  },
  done: (results) => {
    resetCSS();
    resolve(results);
  },
});

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

התוצאות

הרצת מדדי הביצועים האלה עם 20 חזרות על MacBook Pro 2021‏ (Apple M1 Pro) עם 16GB של RAM מניבה את הערכים הממוצעים הבאים:

  • נכס רגיל שעובר בירושה (accent-color): 163 הפעלות לשנייה (‎= 6.13ms להפעלה)
  • נכס מותאם אישית לא רשום (--unregistered): 256 הפעלות לשנייה (= 3.90 אלפיות שנייה)
  • מאפיין מותאם אישית רשום עם inherits: true‏ (--registered): 252 הפעלות לשנייה (‎= 3.96ms להפעלה)

בכמה הפעלות נקודות ההשוואה מניבות תוצאות דומות.

תרשים עמודות עם התוצאות של נכסים שעוברים בירושה. ככל שהמספר גבוה יותר, הביצועים מהירים יותר.
איור: תרשים עמודות עם התוצאות של נכסים שעוברים בירושה. מספרים גבוהים יותר מניבים ביצועים מהירים יותר.

התוצאות מראות שהעלות של רישום נכס מותאם אישית היא זניחה בהשוואה לעלות של אי-רישום הנכס המותאם אישית. מאפיינים מותאמים אישית רשומים שעוברים בירושה פועלים במהירות של 98% מהמהירות של מאפיינים מותאמים אישית לא רשומים. במספרים מוחלטים, רישום המאפיין המותאם אישית מוסיף זמן אחזור של 0.06 אלפיות השנייה.

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

המאפיינים הבאים שצריך לבדוק הם אלה שלא עוברים בירושה. כאן יש רק שני סוגים של נכסים שאפשר לבצע להם השוואה למדד:

  • נכס רגיל שלא עובר בירושה: z-index.
  • מאפיין מותאם אישית רשום עם inherits: false: --registered-no-inherit.

נכסים מותאמים אישית שלא רשומים לא יכולים להיכלל במדד הזה כי הנכסים האלה תמיד יורשים.

נקודות השוואה

מדדי הביצועים דומים מאוד לתרחישים הקודמים. בבדיקה עם --registered-no-inherit, רישום הנכס הבא מוחדר בשלב bootstrap של בדיקת הביצועים:

@property --registered-no-inherit {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

התוצאות

הרצת מדדי הביצועים האלה עם 20 חזרות על MacBook Pro 2021‏ (Apple M1 Pro) עם 16GB של RAM מניבה את הערכים הממוצעים הבאים:

  • מאפיין רגיל שלא עובר בירושה: 290,269 הפעלות לשנייה (‎= 3.44µs להפעלה)
  • מאפיין מותאם אישית רשום שלא עובר בירושה: 214,110 הפעלות לשנייה (‎= 4.67µs להפעלה)

הבדיקה בוצעה כמה פעמים והתוצאות האלה היו אופייניות.

תרשים עמודות עם התוצאות של נכסים שלא עוברים בירושה. מספרים גבוהים יותר מניבים ביצועים מהירים יותר.
איור: תרשים עמודות עם התוצאות של נכסים שלא יורשים. ככל שהמספר גבוה יותר, הביצועים מהירים יותר.

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

  • בנכסים רגילים, מספר ההפעלות עלה מ-163 הפעלות לשנייה ליותר מ-290 אלף הפעלות לשנייה – עלייה של 1,780% בביצועים!
  • בנכסים מותאמים אישית, מספר ההפעלות עלה מ-252 הפעלות לשנייה ליותר מ-214 אלף הפעלות לשנייה – עלייה של 848% בביצועים!

המסקנה העיקרית היא שלשימוש ב-inherits: false כשרושמים מאפיין מותאם אישית יש השפעה משמעותית. אם יש לכם אפשרות לרשום את הנכס בהתאמה אישית ב-inherits: false, כדאי לעשות זאת.

נקודת השוואה של בונוס: רישומים מרובים של נכסים מותאמים אישית

כדאי גם לבדוק את ההשפעה של רישום של הרבה נכסים מותאמים אישית. כדי לעשות זאת, מריצים מחדש את הבדיקה עם --registered-no-inherit, תוך ביצוע 25,000 רישומים אחרים של נכסים מותאמים אישית מראש. הנכסים המותאמים אישית האלה משמשים ב-:root.

ההרשמות האלה מתבצעות בשלב setup של בדיקת הביצועים:

setup: () => {
  const propertyRegistrations = [];
  const declarations = [];

  for (let i = 0; i < 25000; i++) {
    propertyRegistrations.push(`@property --custom-${i} { syntax: "<number>"; initial-value: 0; inherits: true; }`);
    declarations.push(`--custom-${i}: ${Math.random()}`);
  }

  setCSS(`${propertyRegistrations.join("\n")}
  :root {
    ${declarations.join("\n")}
  }`);
},

מספר ההפעלות בשנייה לנקודת ההשוואה הזו דומה מאוד לתוצאה של המדד 'נכס מותאם אישית רשום שלא יורש' (214,110 הפעלות לשנייה לעומת 213,158 הפעלות לשנייה), אבל זה לא החלק המעניין שצריך לבחון. אחרי הכול, סביר להניח ששינוי של נכס מותאם אישית אחד לא יושפע מרישומים מנכסים אחרים.

החלק המעניין בבדיקה הזו הוא למדוד את ההשפעה של הרישומים עצמם. בדפדפן DevTools אפשר לראות של-25,000 רישומי נכסים מותאמים אישית יש עלות ראשונית של חישוב מחדש של סגנונות בסך קצת יותר מ-30ms. לאחר מכן, לא תהיה יותר השפעה על הדברים בגלל הרישומים האלה.

צילום מסך של DevTools עם הדגשה של העלות של &#39;חישוב מחדש של סגנון&#39; עבור 25,000 רישומי נכסים מותאמים אישית. בהסבר הקצר מצוין שהפעולה ארכה 32.42 אלפיות השנייה
איור: צילום מסך של DevTools שבו מודגש העלות של 'חישוב מחדש של סגנון' עבור רישום של 25,000 נכסים מותאמים אישית. בהסבר הקצר מצוין שהפעולה נמשכה 32.42ms

סיכום והמלצות

לסיכום, יש שלושה דברים שאפשר ללמוד מכל זה:

  • רישום נכס מותאם אישית ב-@property כרוך בעלות קלה של ביצועים. בדרך כלל העלות הזו זניחה, כי רישום נכסים מותאמים אישית מאפשר לנצל את מלוא הפוטנציאל שלהם. אי אפשר להשיג את זה בלי לעשות את זה.

  • לשימוש ב-inherits: false כשרושמים מאפיין מותאם אישית יש השפעה משמעותית. כך אפשר למנוע את העברת המאפיין בירושה. לכן, כשערך המאפיין משתנה, הוא משפיע רק על הסגנונות של הרכיב התואם במקום על כל עץ המשנה.

  • מספר ההרשמות ל-@property לא משפיע על חישוב הסגנון מחדש. יש רק עלות קטנה מאוד מראש כשמבצעים את הרישומים, אבל אחרי זה הכול מוכן.