सीएसएस @property की परफ़ॉर्मेंस का मानदंड

पब्लिश करने की तारीख: 2 अक्टूबर, 2024

सीएसएस की किसी नई सुविधा का इस्तेमाल शुरू करते समय, यह समझना ज़रूरी है कि इसका आपकी वेबसाइटों की परफ़ॉर्मेंस पर क्या असर पड़ेगा. भले ही, वह असर अच्छा हो या बुरा. @property अब बेसलाइन में है. इस पोस्ट में, इसकी परफ़ॉर्मेंस पर पड़ने वाले असर के बारे में बताया गया है. साथ ही, इस असर को रोकने के लिए, कुछ कार्रवाइयां करने के बारे में भी बताया गया है.

PerfTestRunner की मदद से सीएसएस की परफ़ॉर्मेंस का आकलन करना

सीएसएस की परफ़ॉर्मेंस का आकलन करने के लिए, हमने "सीएसएस सिलेक्टर बेंचमार्क" टेस्ट सुइट बनाया है. यह Chromium के PerfTestRunner पर आधारित है और सीएसएस की परफ़ॉर्मेंस पर पड़ने वाले असर का आकलन करता है. यह PerfTestRunner वह है जिसका इस्तेमाल Blink–Chromium का रेंडरिंग इंजन, अपने इंटरनल परफ़ॉर्मेंस की जांच करने के लिए करता है.

रनर में 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 फ़ंक्शन, मुख्य हिस्सा है जिसे मेज़र किया जाता है.

सीएसएस सिलेक्टर के मानदंड के लिए डीओएम ट्री की ज़रूरत होती है

सीएसएस सेलेक्टर की परफ़ॉर्मेंस, डीओएम के साइज़ पर भी निर्भर करती है. इसलिए, इन मानदंडों के लिए डीओएम ट्री का साइज़ सही होना चाहिए. इस डीओएम ट्री को मैन्युअल तरीके से बनाने के बजाय, यह ट्री जनरेट हो जाता है.

उदाहरण के लिए, यहां दिया गया 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);

सीएसएस सिलेक्टर के मानदंड, डीओएम ट्री में बदलाव नहीं करते हैं. इसलिए, किसी भी बेंचमार्क के चलने से पहले, यह ट्री जनरेशन सिर्फ़ एक बार एक्ज़ीक्यूट होता है.

मानदंड चलाना

टेस्ट सुइट का हिस्सा होने वाला कोई मानदंड चलाने के लिए, आपको सबसे पहले वेब सर्वर शुरू करना होगा:

npm run start

शुरू करने के बाद, बेंचमार्क के पब्लिश किए गए यूआरएल पर जाकर, window.startTest() को मैन्युअल तरीके से चलाया जा सकता है.

किसी भी एक्सटेंशन या अन्य फ़ैक्टर के बिना, इन बेंचमार्क को अलग से चलाने के लिए, Puppeteer को सीएलआई से ट्रिगर किया जाता है. इससे बेंचमार्क में पास किए गए दस्तावेज़ को लोड और लागू किया जाता है.

इन @property मानदंडों के लिए, सीएलआई पर ये निर्देश लागू करें. इसके लिए, काम के पेज के यूआरएल पर जाने के बजाय, सीएलआई पर ये निर्देश लागू करें:

npm run benchmark at-rule/at-property

यह Puppeteer की मदद से पेज को लोड करता है, window.startTest() को अपने-आप कॉल करता है, और नतीजों की रिपोर्ट करता है.

सीएसएस प्रॉपर्टी की परफ़ॉर्मेंस का मानदंड

किसी सीएसएस प्रॉपर्टी की परफ़ॉर्मेंस का आकलन करने के लिए, यह देखा जाता है कि वह स्टाइल अमान्य होने की स्थिति को कितनी तेज़ी से मैनेज कर सकती है. साथ ही, ब्राउज़र को स्टाइल को फिर से कैलकुलेट करने के लिए, कितनी देर लगती है.

स्टाइल के अमान्य होने पर, यह मार्क किया जाता है कि डीओएम में बदलाव होने पर, किन एलिमेंट की स्टाइल को फिर से कैलकुलेट करना ज़रूरी है. हर बदलाव के जवाब में, सब कुछ अमान्य कर देना सबसे आसान तरीका है.

ऐसा करते समय, इनहेरिट करने वाली सीएसएस प्रॉपर्टी और इनहेरिट न करने वाली सीएसएस प्रॉपर्टी के बीच अंतर करना ज़रूरी है.

  • जब किसी टारगेट किए गए एलिमेंट पर, इनहेरिट की गई सीएसएस प्रॉपर्टी में बदलाव होता है, तो टारगेट किए गए एलिमेंट के नीचे मौजूद सबट्री में मौजूद सभी एलिमेंट की स्टाइल में भी बदलाव करना पड़ता है.
  • जब टारगेट किए गए एलिमेंट पर बदलावों को इनहेरिट नहीं करने वाली सीएसएस प्रॉपर्टी का इस्तेमाल किया जाता है, तो सिर्फ़ उस एलिमेंट की स्टाइल अमान्य हो जाती हैं.

इनहेरिट करने वाली प्रॉपर्टी की तुलना, इनहेरिट न करने वाली प्रॉपर्टी से करना सही नहीं होगा. इसलिए, बेंचमार्क के दो सेट चलाए जाते हैं:

  • इनहेरिट की गई प्रॉपर्टी वाला मानदंडों का सेट.
  • ऐसी प्रॉपर्टी वाले मानदंडों का सेट जो इनहेरिट नहीं होती हैं.

यह जानना बहुत ज़रूरी है कि किन प्रॉपर्टी को बेंचमार्क किया जाना चाहिए. कुछ प्रॉपर्टी (जैसे कि accent-color) सिर्फ़ स्टाइल को अमान्य बनाती हैं. वहीं, कई प्रॉपर्टी (जैसे कि writing-mode) लेआउट या पेंट जैसी अन्य चीज़ों को भी अमान्य बनाती हैं. आपको सिर्फ़ ऐसी प्रॉपर्टी चाहिए जो स्टाइल को अमान्य कर सकें.

यह पता लगाने के लिए, Blink की सीएसएस प्रॉपर्टी की सूची देखें. हर प्रॉपर्टी में एक फ़ील्ड invalidate होता है, जिसमें यह जानकारी होती है कि किन चीज़ों को अमान्य कर दिया गया है.

इसके अलावा, यह भी ज़रूरी है कि सूची में से ऐसी प्रॉपर्टी चुनी जाए जिसे independent के तौर पर मार्क न किया गया हो. ऐसा इसलिए, क्योंकि ऐसी प्रॉपर्टी की तुलना करने से नतीजों में गड़बड़ी हो सकती है. इंडिपेंडेंट प्रॉपर्टी का, दूसरी प्रॉपर्टी या फ़्लैग पर कोई खराब असर नहीं पड़ता है. जब सिर्फ़ इंडिपेंडेंट प्रॉपर्टी में बदलाव होता है, तो Blink एक तेज़ कोड-पाथ का इस्तेमाल करता है. यह कोड-पाथ, डिसेंटेंट की स्टाइल को क्लोन करता है और क्लोन की गई कॉपी में नई वैल्यू अपडेट करता है. यह तरीका, पूरी गिनती फिर से करने से ज़्यादा तेज़ है.

इनहेरिट की गई सीएसएस प्रॉपर्टी की परफ़ॉर्मेंस का बेंचमार्क बनाना

मानदंडों का पहला सेट, इनहेरिट की गई सीएसएस प्रॉपर्टी पर फ़ोकस करता है. तीन तरह की प्रॉपर्टी होती हैं, जिन्हें टेस्ट करने और एक-दूसरे से तुलना करने के लिए इनहेरिट किया जाता है:

  • एक सामान्य प्रॉपर्टी, जो इनहेरिट करती है: accent-color.
  • रजिस्टर नहीं की गई कस्टम प्रॉपर्टी: --unregistered.
  • inherits: true: --registered के साथ रजिस्टर की गई कस्टम प्रॉपर्टी.

रजिस्टर नहीं की गई कस्टम प्रॉपर्टी इस सूची में जोड़ दी जाती हैं, क्योंकि ये डिफ़ॉल्ट रूप से इनहेरिट की जाती हैं.

जैसा कि पहले बताया गया है, इनहेरिट की गई प्रॉपर्टी को ध्यान से चुना गया था, ताकि यह सिर्फ़ स्टाइल को अमान्य कर सके और independent के तौर पर मार्क न की गई हो.

रजिस्टर की गई कस्टम प्रॉपर्टी के लिए, इस रन में सिर्फ़ उन प्रॉपर्टी की जांच की जाती है जिनका inherits डिस्क्रिप्टर 'सही' पर सेट होता है. inherits डिस्क्रिप्टर से यह तय होता है कि प्रॉपर्टी, चाइल्ड प्रॉपर्टी को इनहेरिट करती है या नहीं. इससे कोई फ़र्क़ नहीं पड़ता कि इस प्रॉपर्टी को सीएसएस @property या JavaScript CSS.registerProperty के ज़रिए रजिस्टर किया गया है, क्योंकि रजिस्ट्रेशन, बेंचमार्क का हिस्सा नहीं है.

मानदंड

जैसा कि पहले ही बताया जा चुका है, मानदंड वाला पेज डीओएम ट्री बनाकर शुरू होता है, ताकि बदलावों का कोई भी असर देखने के लिए, पेज में नोड का काफ़ी बड़ा सेट हो.

हर मानदंड, किसी प्रॉपर्टी की वैल्यू बदलता है. इसके बाद, यह स्टाइल अमान्य होने की प्रोसेस को ट्रिगर करता है. बेंचमार्क से यह पता चलता है कि पेज का आकलन फिर से करने में, अमान्य स्टाइल का आकलन करने में कितना समय लगता है.

एक बेंचमार्क पूरा होने के बाद, इंजेक्ट की गई सभी स्टाइल रीसेट हो जाती हैं, ताकि अगला बेंचमार्क शुरू किया जा सके.

उदाहरण के लिए, --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 खाली होता है, क्योंकि रजिस्टर करने के लिए कोई प्रॉपर्टी नहीं होती.

नतीजे

16 जीबी रैम वाले 2021 MacBook Pro (Apple M1 Pro) पर, इन बेंचमार्क को 20 बार चलाने पर, ये औसत मिलते हैं:

  • इनहेरिट करने वाली सामान्य प्रॉपर्टी (accent-color): 163 रन प्रति सेकंड (= 6.13 मि॰से॰ हर रन)
  • रजिस्टर नहीं की गई कस्टम प्रॉपर्टी (--unregistered): हर सेकंड 256 बार (= हर बार 3.90 मिलीसेकंड)
  • inherits: true (--registered) के साथ रजिस्टर की गई कस्टम प्रॉपर्टी: हर सेकंड 252 रन (= हर रन 3.96 मिलीसेकंड)

कई बार चलाने पर, बेंचमार्क मिलते-जुलते नतीजे देते हैं.

इनहेरिट की गई प्रॉपर्टी के नतीजों वाला बार चार्ट. ज़्यादा संख्याओं वाले फ़ॉर्मूले तेज़ी से काम करते हैं.
इमेज: इनहेरिट की गई प्रॉपर्टी के नतीजों वाला बार चार्ट. ज़्यादा संख्याओं वाले फ़ॉर्मूले तेज़ी से काम करते हैं.

नतीजों से पता चलता है कि कस्टम प्रॉपर्टी को रजिस्टर करने की लागत, उसे रजिस्टर न करने की लागत की तुलना में बहुत कम होती है. रजिस्टर की गई कस्टम प्रॉपर्टी, इनहेरिट की गई रजिस्टर नहीं की गई कस्टम प्रॉपर्टी की गति के 98% पर चलती हैं. कस्टम प्रॉपर्टी को रजिस्टर करने पर, कुल समय में 0.06 मिलीसेकंड का ओवरहेड जुड़ता है.

इनहेरिट न करने वाली सीएसएस प्रॉपर्टी की परफ़ॉर्मेंस का आकलन करना

अगली प्रॉपर्टी, वे होती हैं जो इनहेरिट नहीं होती हैं. यहां सिर्फ़ दो तरह की प्रॉपर्टी को बेंचमार्क किया जा सकता है:

  • सामान्य प्रॉपर्टी, जो इनहेरिट नहीं करती: z-index.
  • inherits: false: --registered-no-inherit के साथ रजिस्टर की गई कस्टम प्रॉपर्टी.

रजिस्टर नहीं की गई कस्टम प्रॉपर्टी, इस बेंचमार्क का हिस्सा नहीं हो सकतीं, क्योंकि वे प्रॉपर्टी हमेशा इनहेरिट होती हैं.

मानदंड

ये मानदंड, पिछले उदाहरणों से काफ़ी मिलते-जुलते हैं. --registered-no-inherit की मदद से टेस्ट करने के लिए, यहां दिए गए प्रॉपर्टी रजिस्ट्रेशन को मानदंड के bootstrap फ़ेज़ में शामिल किया गया है:

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

नतीजे

इन बेंचमार्क को 16 जीबी रैम वाले 2021 MacBook Pro (Apple M1 Pro) फ़ोन पर, 20 बार चलाने के हिसाब से औसत नतीजे मिलते हैं:

  • ऐसी सामान्य प्रॉपर्टी जो इनहेरिट नहीं करती: हर सेकंड 2,90,269 बार चलती है (= हर रन 3.44µs)
  • रजिस्टर की गई कस्टम प्रॉपर्टी, जो इनहेरिट नहीं करती: हर सेकंड 2,14,110 रन (= हर रन 4.67µs)

इस टेस्ट को कई बार दोहराया गया और ये सामान्य नतीजे मिले.

जो प्रॉपर्टी इनहेरिट नहीं होती उनके नतीजों वाला बार चार्ट. ज़्यादा संख्याओं वाले फ़ॉर्मूले तेज़ी से काम करते हैं.
इमेज: बार चार्ट में, उन प्रॉपर्टी के नतीजे दिखाए जाते हैं जो इनहेरिट नहीं होती हैं. ज़्यादा संख्याओं वाले फ़ॉर्मूले तेज़ी से काम करते हैं.

यहां सबसे अहम बात यह है कि इनहेरिट न करने वाली प्रॉपर्टी, इनहेरिट करने वाली प्रॉपर्टी की तुलना में काफ़ी तेज़ी से काम करती हैं. सामान्य प्रॉपर्टी के लिए ऐसा होना लाज़िमी था, लेकिन यह बात कस्टम प्रॉपर्टी के लिए भी लागू होती है.

  • सामान्य प्रॉपर्टी के लिए, हर सेकंड में 163 से 2,90,000 से ज़्यादा रन हुए. इसका मतलब है कि परफ़ॉर्मेंस में 1,780% की बढ़ोतरी हुई!
  • कस्टम प्रॉपर्टी के लिए, हर सेकंड में होने वाले रन की संख्या 252 से बढ़कर 2,14,000 से ज़्यादा हो गई. इसका मतलब है कि परफ़ॉर्मेंस में 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")}
  }`);
},

इस बेंचमार्क के लिए, हर सेकंड में होने वाले रन की संख्या, "रजिस्टर की गई कस्टम प्रॉपर्टी जो इनहेरिट नहीं करती" के नतीजे से काफ़ी मिलती-जुलती है (हर सेकंड में 2,14,110 रन बनाम हर सेकंड में 2,13,158 रन). हालांकि, यह देखने वाली दिलचस्प बात नहीं है. ऐसा हो सकता है कि एक कस्टम प्रॉपर्टी को बदलने पर, दूसरी प्रॉपर्टी के रजिस्ट्रेशन पर कोई असर नहीं पड़ेगा.

इस टेस्ट का दिलचस्प हिस्सा, रजिस्ट्रेशन के असर को मेज़र करना है. DevTools में जाकर, यह देखा जा सकता है कि 25,000 कस्टम प्रॉपर्टी रजिस्ट्रेशन के लिए, स्टाइल को फिर से कैलकुलेट करने की शुरुआती लागत 30ms से थोड़ी ज़्यादा है. ऐसा होने के बाद, इन रजिस्ट्रेशन की मौजूदगी से चीज़ों पर कोई असर नहीं पड़ता.

DevTools का स्क्रीनशॉट, जिसमें 25 हज़ार कस्टम प्रॉपर्टी रजिस्ट्रेशन करने के लिए, &#39;स्टाइल फिर से कैलकुलेट करें&#39; की लागत को हाइलाइट किया गया है. टूलटिप से पता चलता है कि प्रोसेस करने में 32.42 मि॰से॰ लगे
इमेज: DevTools का स्क्रीनशॉट, जिसमें 25 हज़ार कस्टम प्रॉपर्टी रजिस्ट्रेशन को हाइलाइट करने के लिए, "स्टाइल का फिर से हिसाब लगाएं" शुल्क दिखाया गया है. टूलटिप से पता चलता है कि इसमें 32.42ms
समय लगा

नतीजा और अहम जानकारी

खास जानकारी के तौर पर, इन तीन बातों का ध्यान रखें:

  • @property के साथ कस्टम प्रॉपर्टी रजिस्टर करने पर, परफ़ॉर्मेंस पर थोड़ा असर पड़ता है. आम तौर पर, यह शुल्क मामूली होता है. इसकी वजह यह है कि कस्टम प्रॉपर्टी रजिस्टर करने पर, उनकी पूरी क्षमता का इस्तेमाल किया जा सकता है. ऐसा बिना रजिस्टर किए नहीं किया जा सकता.

  • कस्टम प्रॉपर्टी को रजिस्टर करते समय inherits: false का इस्तेमाल करने से काफ़ी फ़ायदा होता है. इसकी मदद से, प्रॉपर्टी को इनहेरिट होने से रोका जा सकता है. इसलिए, जब प्रॉपर्टी की वैल्यू बदलती है, तो इसका असर पूरे सबट्री के बजाय, सिर्फ़ मैच किए गए एलिमेंट की स्टाइल पर पड़ता है.

  • बहुत कम या बहुत ज़्यादा @property रजिस्ट्रेशन होने से, स्टाइल को फिर से कैलकुलेट करने पर कोई असर नहीं पड़ता. रजिस्ट्रेशन के लिए, आपको पहले सिर्फ़ थोड़ी सी रकम चुकानी होगी. इसके बाद, आपको कुछ नहीं देना होगा.