हमने अपने प्लगइन के प्रदर्शन को 200% कैसे बढ़ाया

द्वारा लिखित

में

,

पिछले 2 फरवरी, 2017 को, एन टेलर ने CodeInWP में 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right लेख लिखा था। हम एक साल से भी अधिक समय से Page Builder Sandwich में लगातार सुधार कर रहे हैं: हम अपना शोध करते हैं, अपने ग्राहकों की बात सुनते हैं, इसलिए हमने सोचा था कि Page Builder Sandwich CodeInWP के “Who Got it Right” लेख में जगह बना लेगा। हालांकि, परफॉर्मेंस की वजह से ऐसा नहीं हो पाया। ?

इसलिए हमने एन से इसके बारे में पूछा, और उन्होंने उत्तर दिया:

…मैंने इसके मुफ़्त वर्शन को थोड़ा आज़माया। यह काफी आशाजनक लगता है: बहुत सारे मॉड्यूल, काफी सहज यूआई (UI), बेहतरीन WYSIWYG अनुभव, सब कुछ आसानी से ड्रैग हो जाता है। हालाँकि, जब मेरे पेज पर लगभग 10 मॉड्यूल थे, तो धीमी संपादन गति (editing speed) के कारण इसके साथ काम जारी रखना कठिन हो गया…

तो उनके अनुसार, Page Builder Sandwich में परफॉर्मेंस संबंधी समस्याएं हैं। और चूँकि हम हमेशा फीडबैक पर ध्यान देते हैं, हमने इसकी जांच की।

परफॉर्मेंस की टेस्टिंग

हमें PBS के परफॉर्मेंस की जांच करने के लिए एक तरीके की आवश्यकता थी; चूँकि PBS मुख्य रूप से जावास्क्रिप्ट (Javascript) के माध्यम से ब्राउज़र पर चलता है, इसलिए Chrome का Timeline profiler इस काम के लिए सबसे सही टूल था। टाइमलाइन प्रोफाइलर यह पहचान और माप सकता है कि रनटाइम के दौरान वेबपेज में कौन सी प्रक्रियाएं हो रही हैं, जो स्पीड बॉटलनेक का पता लगाने के लिए बिल्कुल उपयुक्त है।

हमने सोचा कि हमें पहले सबसे आसान स्टेप की टेस्टिंग करनी चाहिए – अपने माउस को कई एलिमेंट्स और पंक्तियों के ऊपर ले जाना। सरल है, है ना? हमें पूरा विश्वास था कि इतनी सरल चीज़ में हमें परफॉर्मेंस से जुड़ी समस्याओं का सामना नहीं करना पड़ेगा। दुर्भाग्य से, हम गलत थे।

जैसे ही हमने रिकॉर्ड बटन दबाया, हमने स्क्रीन पर लगभग 2 सेकंड तक माउस घुमाया, फिर स्टॉप बटन दबा दिया।  प्रोफाइलर से प्राप्त परिणामी टाइमलाइन यहाँ दी गई है:

और देखिए! यह परफॉर्मेंस बॉटलनेक्स से पूरी तरह भरा हुआ है!

ग्राफ़ में बहुत कुछ हो रहा है, लेकिन सबसे स्पष्ट समस्या यह है कि इसमें बहुत सारे बैंगनी क्षेत्र हैं (जो कि नहीं होने चाहिए)। इसके अलावा, ये बैंगनी क्षेत्र दिखने में काफी चौड़े हैं, और ऐसा लगता है कि वे सभी ग्राफ़ के चरम स्तर (peak) को छू रहे हैं। आप यह भी देखेंगे कि ग्राफ़ हमेशा भरा हुआ रहता है, और बैंगनी क्षेत्र लगभग कभी भी शून्य तक नहीं आते हैं। ऐसा लगता है जैसे ब्राउज़र हमेशा कुछ न कुछ करने में व्यस्त रहता है, और लगभग कभी आराम नहीं करता।

लेकिन उन सभी बैंगनी क्षेत्रों का वास्तव में क्या मतलब है?

प्रोफाइलर से हमें पता चला कि इन्हें रिफ्लो (reflows) कहा जाता है। Google डेवलपर साइट:

दस्तावेज़ में तत्वों (elements) की स्थितियों और ज्यामिति (geometries) की पुनर्गणना करने की वेब ब्राउज़र प्रक्रिया का नाम रिफ्लो है.. रिफ्लो ब्राउज़र में उपयोगकर्ता को ब्लॉक करने वाला एक ऑपरेशन (user-blocking operation) है।

तो इसका मतलब यह है कि PBS में माउस को इधर-उधर घुमाने से ढेरों रिफ्लो ट्रिगर होते हैं। इससे भी बुरी बात यह है कि प्रत्येक रिफ्लो ब्राउज़र को थोड़ी देर के लिए रोक देता है ताकि वह अपनी गणना पूरी कर सके। अधिक रिफ्लो का अर्थ है अधिक बार ब्राउज़र का रुकना, और अनुभव और भी अधिक झटकेदार (janky) हो जाता है। सरल शब्दों में कहें तो, बहुत सारे बैंगनी क्षेत्र = खराब।

तो यह साबित हुआ कि एन सही थीं: हमारे पास वास्तव में एक बहुत बड़ा परफॉर्मेंस बॉटलनेक था, इसलिए हमारी टीम इसे तुरंत ठीक करने के काम में जुट गई।

हमारी रिफ्लो / जेंक (Jank) समस्या का समाधान

किसी भी अन्य चीज़ से पहले, हमें यह जानने के लिए कुछ शोध करने की आवश्यकता थी कि वास्तव में इस रिफ्लो का कारण क्या है। हमें पता चला कि जाहिर तौर पर, जावास्क्रिप्ट में बहुत सी सामान्य चीजें ब्राउज़र को रिफ्लो करने के लिए उकसा सकती हैं।

लगभग कोई भी फ़ंक्शन जिसका उपयोग आप DOM एलिमेंट की चौड़ाई, ऊँचाई या स्थिति प्राप्त करने के लिए कर सकते हैं, रिफ्लो को ट्रिगर कर सकता है। पॉल आयरिश ने जावास्क्रिप्ट के उन सभी प्रॉपर्टीज़ और फ़ंक्शन्स की एक विस्तृत सूची लिखी है जो ऐसा कर सकते हैं। बुरी खबर यह है कि Page Builder Sandwich इनमें से कई प्रॉपर्टीज़ और फ़ंक्शन्स का उपयोग करता है, और वे सभी रिफ्लो का कारण बनते हैं:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • और भी बहुत कुछ…

समाधान #1: अत्यधिक संसाधन लेने वाली रिफ्लो प्रक्रियाओं को कैश (Cache) करना

सबसे स्पष्ट समाधान उन प्रॉपर्टीज़ और फ़ंक्शन्स का उपयोग न करना है। हालाँकि, PBS उन पर काफी निर्भर करता है और हम उनके बिना काम नहीं चला सकते।

हमारे पास जो समाधान बचा है वह यह है कि उन फ़ंक्शन्स के कॉल्स को जितना संभव हो उतना कम किया जाए। यहाँ कठिन बात यह थी कि हमने PBS के विभिन्न हिस्सों में उन प्रॉपर्टीज़ और फ़ंक्शन्स का कई बार उपयोग किया था, और अधिकांश समय, हमने एक ही DOM एलिमेंट पर कई बार उनका उपयोग किया था। इसे ठीक करने के लिए, हमने प्रत्येक समस्याग्रस्त प्रॉपर्टी/फ़ंक्शन के लिए अपना खुद का कैशिंग तंत्र बनाया।

उदाहरण के लिए, getBoundingClientRect  फ़ंक्शन हमें DOM एलिमेंट का वर्तमान टॉप, लेफ्ट, चौड़ाई, ऊँचाई, राइट और बॉटम पिक्सेल माप देता है। हम PBS में एलिमेंट की आउटलाइन कहाँ खींचनी है, इसकी गणना करने के लिए हर समय इस फ़ंक्शन का उपयोग करते हैं। इसके लिए हमने जो कैशिंग फ़ंक्शन बनाया है, वह यहाँ है:

// Cache: Our client rects are kept here, we can use this to clear them later.
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// Check if we already got the client rect before.
	if ( ! element._boundingClientRect ) {

		// If not, get it then store it for future use.
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

getBoundingClientRect  को कॉल करने के बजाय, अब हमने pbsGetBoundingClientRect  का उपयोग किया। अब हर बार जब क्लाइंट रेक्ट (client rect) की क्वेरी की जाती है, तो हम उस मान को भविष्य के संदर्भ के लिए संग्रहीत कर लेते हैं, जिससे रिफ्लो ट्रिगर्स की संख्या सीमित हो जाती है।

जब संग्रहीत क्लाइंट रेक्ट अमान्य हो जाते हैं, तो हमें कैश को साफ़ करने के तरीके की भी आवश्यकता थी। जब उपयोगकर्ता पेज को स्क्रॉल करता है या स्क्रीन में तत्वों को जोड़ता/बदलता/हटाता है, तो संग्रहीत क्लाइंट रेक्ट के मान बदल जाते हैं। इसलिए हमने एक कैश क्लियरिंग फ़ंक्शन बनाया जिसे हम जब भी नए क्लाइंट रेक्ट मानों की आवश्यकता हो, कॉल कर सकते हैं:

var clearClientRects = function() {
	var i;
	for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
		if ( elemsWithBoundingRects[ i ] ) {
			elemsWithBoundingRects[ i ]._boundingClientRect = null;
		}
	}
	elemsWithBoundingRects = [];
};​

हमने अन्य भारी फ़ंक्शन कॉल्स के लिए भी यही कैशिंग तंत्र लागू किया जिनका हम आमतौर पर उपयोग करते हैं।

समाधान #2: FastDom का उपयोग करके Reads और Writes को एक साथ समूहीकृत करना

समाधान #1 को लागू करने से PBS का परफॉर्मेंस बढ़ा, लेकिन यह पर्याप्त नहीं था।

थोड़ा और गूगल करने पर, हमें ‘layout thrashing’ की रोकथाम पर विल्सन पेज का एक लेख मिला। विल्सन कहते हैं:

लेआउट थ्रैशिंग (रिफ्लो) तब होती है जब जावास्क्रिप्ट बार-बार DOM में तेज़ी से लिखता (writes) है और फिर पढ़ता (reads) है, जिससे डॉक्यूमेंट में रिफ्लो होता है।

जाहिर तौर पर, विशिष्ट रिफ्लो-ट्रिगर करने वाली प्रॉपर्टीज़ और फ़ंक्शन्स पर अपने कॉल्स को सीमित करना ही पर्याप्त नहीं है। जब आप DOM पर लगातार कई “reads” और “writes” करते हैं, तो यह ब्राउज़र रिफ्लो को भी ट्रिगर करता है। यहाँ सीधे विल्सन की साइट से एक विशिष्ट परिदृश्य दिया गया है:

// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers reflow)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

// Read (triggers reflow)
var h3 = element3.clientHeight;

// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame​

ऊपर दिया गया उदाहरण 3 रिफ्लो को ट्रिगर करता है क्योंकि लिखने (write) के बाद पूरे पेज के आयाम अमान्य हो जाते हैं। इसके लिए सुझाया गया समाधान कोड को पुनर्व्यवस्थित करके सभी reads और writes को एक साथ बंच (एकत्र) करना है ताकि पूरी प्रक्रिया केवल 1 ब्राउज़र रिफ्लो को ही ट्रिगर करे:

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame​

ऊपर दिया गया स्निपेट समस्या को ठीक करना आसान दिखाता है। लेकिन वास्तव में, PBS में, रिफ्लो ट्रिगर करने वाली चीज़ों को पढ़ना और लिखना PBS के लाइफ़-साइकिल के दौरान कई अलग-अलग जगहों पर और अलग-अलग समय पर होता है। जावास्क्रिप्ट कोड की हज़ारों पंक्तियों को फिर से व्यवस्थित करना संभव नहीं है।

सौभाग्य से हमारे लिए, विल्सन ने अपने लेख में एक समाधान भी प्रदान किया था। उन्होंने उन reads और writes को एक साथ बंच करने में मदद के लिए एक छोटी यूटिलिटी स्क्रिप्ट बनाई, जिसे FastDom कहा जाता है।

Page Builder Sandwich का कुछ कोड यहाँ दिया गया है जो FastDom का उपयोग करता है और इसमें हमारा उपरोक्त समाधान #1 भी शामिल है:

// Bunch together the reads from other threads.
fastdom.measure( function() {

	// Use our cached client rect.
	var rect = window.pbsGetBoundingClientRect( elem );

	// Bunch together the writes from other threads.
	fastdom.mutate( function() {
		elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
		elem.style.height = rect.height + 'px';
		elem.style.left = rect.left + 'px';
		elem.style.width = rect.width + 'px';
	}.bind( this ) );
}.bind( this ) );​

FastDom यह करता है कि यह सभी fastdom.measure  कॉल्स और सभी fastdom.mutate  कॉल्स को एक साथ समूहीकृत करता है और उन्हें एसिंक्रोनस रूप से निष्पादित करता है, जिससे ब्राउज़र में सभी रिफ्लो को कम करने के लिए सभी reads और writes प्रभावी रूप से एक साथ आ जाते हैं।

समाधान #3: स्टाइल ऑब्जेक्ट्स को एक्सेस करना महंगा (Costly) है

हमें jsPerf की मदद से यह भी पता चला कि कुछ स्टाइल-प्राप्त करने वाले फ़ंक्शन्स दूसरों की तुलना में तेज़ थे।

PBS में, हमें आमतौर पर किसी एलिमेंट की किसी विशिष्ट स्टाइल का वर्तमान मान प्राप्त करने की आवश्यकता होती है। उदाहरण के लिए, हम सामान्यतः टॉप मार्जिन प्राप्त करने के लिए इसका उपयोग करते हैं:

var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;​

लेकिन, हमें पता चला कि ऐसा करना कहीं अधिक तेज़ है:

var marginTop = elem.style.marginTop;​

हालाँकि, elem.style.marginTop  और getComputedStyle  बहुत भिन्न हैं। पहला हमें केवल एलिमेंट की इनलाइन स्टाइलिंग देता है, और किसी भी स्टाइलशीट को ध्यान में नहीं रखता है; और दूसरा इनलाइन स्टाइल और स्टाइलशीट दोनों को ध्यान में रखता है।

इतना ही नहीं, getComputedStyle  परिणाम कैश होने के बावजूद भी यह बहुत धीमा था। जैसा कि सामने आया, स्टाइल ऑब्जेक्ट को स्वयं एक्सेस करना ही धीमी प्रक्रिया है।

सौभाग्य से हमारे लिए, PBS एलिमेंट्स में इनलाइन स्टाइल जोड़ता है, और चूंकि इनलाइन स्टाइल (अधिकांश समय) स्टाइलशीट से प्राथमिकता लेते हैं, ऐसे मामले होंगे जहां इनलाइन स्टाइल मान प्राप्त करना ही पर्याप्त होगा। इसलिए यदि हम ऊपर दिए गए दोनों तरीकों को मिला दें, तो हम बहुत तेज़ी से चलने वाला कोड बना सकते हैं:

var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
	var styles = window.getComputedStyle( elem );
	marginTop = styles.marginTop;
}​

कोड सुधारों के परिणाम

हमारे उपरोक्त 3 समाधानों में बहुत सारे शोध, परीक्षण और वास्तविक कार्यान्वयन की आवश्यकता पड़ी (और हमारे Fonts फीचर को रिलीज़ होने में देरी हुई), लेकिन अंत में यह सब सार्थक रहा।

स्क्रीन पर 2 सेकंड के लिए माउस को ले जाने के हमारे शुरुआती परीक्षण पर वापस आते हैं। यहाँ पुरानी अप्रिय/झटकेदार टाइमलाइन दी गई है:

और परफॉर्मेंस सुधारों के बाद हमारी नई टाइमलाइन इस तरह दिखती है:

जैसा कि आप देख सकते हैं, ग्राफ़ में बैंगनी क्षेत्र कम हैं। और ब्राउज़र द्वारा की जा रही प्रक्रियाओं की मात्रा में एक उल्लेखनीय अंतर है। नीचे दी गई आयताकार पट्टियाँ भी पतली हैं, जिसका अर्थ है कि प्रक्रियाएं पहले की तुलना में तेज़ी से चल रही हैं।

यदि आप पुराने और नए ग्राफ़ में ऊँचे/चौड़े बैंगनी क्षेत्रों की मात्रा की तुलना करें, तो हमें आश्चर्यजनक रूप से 200% परफॉर्मेंस वृद्धि देखने को मिल रही है! हम हर स्तर पर परफॉर्मेंस में सुधार भी देख रहे हैं। बिल्कुल भी बुरा नहीं है!

मुख्य बातें और सीख

सच कहें तो, एन द्वारा इसका उल्लेख किए जाने से पहले हमें नहीं लगा था कि PBS में परफॉर्मेंस संबंधी समस्याएं थीं। मुख्य रूप से इसलिए क्योंकि हम डेवलपमेंट-केंद्रित मशीनों का उपयोग कर रहे थे और परफॉर्मेंस बॉटलनेक्स इतने स्पष्ट नहीं थे। पीछे मुड़कर देखें, तो परफॉर्मेंस की जांच करना सर्वोच्च प्राथमिकताओं में से एक होना चाहिए था। लेकिन कम से कम अब, वर्शन 4.3 के अनुसार इसका ध्यान रख लिया गया है। ?

जो लोग अपने वर्डप्रेस प्लगइन के परफॉर्मेंस में सुधार करना चाहते हैं, खासकर यदि आप भी भारी जावास्क्रिप्ट का उपयोग कर रहे हैं, तो इस अनुभव से मेरे प्रमुख सुझाव यहाँ दिए गए हैं:

  1. रिफ्लो को ट्रिगर करने वाले प्रॉपर्टीज़/फ़ंक्शन्स के कॉल्स को सीमित/कैश करें। इसे एक गाइड के रूप में उपयोग करें: What forces layout/reflow. The comprehensive list.
  2. DOM स्टाइल्स के “reads” और “writes” को एक साथ समूहीकृत करें। इसे अपने लिए करने हेतु FastDom का उपयोग करें।
  3. Chrome के Timeline profiler का उपयोग करें, और सुनिश्चित करें कि आपके पास कम से कम रिफ्लो (बैंगनी चीजें) हों।