যেভাবে আমরা আমাদের প্লাগইনের কর্মক্ষমতা ২০০% বৃদ্ধি করেছি

লিখেছেন

মধ্যে

,

গত ২ ফেব্রুয়ারি, ২০১৭ তারিখে অ্যান টেলর CodeInWP-তে 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right শীর্ষক নিবন্ধটি লিখেছিলেন। আমরা এখন এক বছরেরও বেশি সময় ধরে পেজ বিল্ডার স্যান্ডউইচ ক্রমাগত উন্নত করে আসছি: আমরা গবেষণা করি, গ্রাহকদের মতামত শুনি, তাই আমরা ভেবেছিলাম পেজ বিল্ডার স্যান্ডউইচ CodeInWP-এর “হু গট ইট রাইট” নিবন্ধে জায়গা করে নেবে। তবে পারফরম্যান্সের সমস্যার কারণে আমরা জায়গা পাইনি। ?

তাই আমরা অ্যানকে এই বিষয়ে জিজ্ঞাসা করেছিলাম এবং তিনি উত্তর দিয়েছিলেন:

…আমি ফ্রি সংস্করণটি কিছুটা ব্যবহার করে দেখেছি। এটি অনেক সম্ভাবনাময়: প্রচুর মডিউল, বেশ স্বজ্ঞাত UI, দুর্দান্ত WYSIWYG অভিজ্ঞতা, সবকিছু সহজেই ড্র্যাগ করা যায়। তবে, যখন আমার পৃষ্ঠায় প্রায় ১০টি মডিউল ছিল, তখন কম এডিটিং স্পিডের কারণে এটির সাথে কাজ চালিয়ে যাওয়া কঠিন হয়ে পড়েছিল…

সুতরাং তার মতে, পেজ বিল্ডার স্যান্ডউইচের পারফরম্যান্স সংক্রান্ত সমস্যা রয়েছে। এবং যেহেতু আমরা সবসময় মতামতের গুরুত্ব দিই, তাই আমরা বিষয়টি খতিয়ে দেখেছি।

পারফরম্যান্স পরীক্ষা করা

PBS-এর পারফরম্যান্স যাচাই করার জন্য আমাদের একটি উপায় প্রয়োজন ছিল; যেহেতু PBS মূলত জাভাস্ক্রিপ্টের মাধ্যমে ব্রাউজারে চলে, তাই এই কাজের জন্য Chrome-এর Timeline profiler ছিল সঠিক টুল। টাইমলাইন প্রোফাইলার রানটাইম চলাকালীন একটি ওয়েবপেজে কোন কোন প্রক্রিয়া সম্পন্ন হচ্ছে তা শনাক্ত ও পরিমাপ করতে পারে, যা গতির বাধাগুলি (স্পিড বটলনেক) খুঁজে বের করার জন্য একদম উপযুক্ত।

আমরা ভেবেছিলাম আমাদের প্রথমে সবচেয়ে সহজ ধাপটি পরীক্ষা করা উচিত – একাধিক এলিমেন্ট এবং রো-এর ওপর দিয়ে মাউস ঘোরানো। সহজ, তাই না? আমরা নিশ্চিত ছিলাম যে এত সহজ একটি বিষয়ে আমরা কোনো পারফরম্যান্স সমস্যায় পড়ব না। দুর্ভাগ্যবশত, আমরা ভুল ছিলাম।

আমরা রেকর্ড বাটনে ক্লিক করার পর, স্ক্রিনের ওপর প্রায় ২ সেকেন্ড মাউস নাড়াচাড়া করে স্টপ বাটনে ক্লিক করলাম। প্রোফাইলার থেকে প্রাপ্ত টাইমলাইন এখানে দেওয়া হলো:

দেখুন কাণ্ড! এটি পারফরম্যান্সের নানা ধরনের জটিলতায় একদম ঠাসা!

গ্রাফটিতে অনেক কিছু ঘটছে, তবে সবচেয়ে স্পষ্ট সমস্যা হলো এতে প্রচুর বেগুনি অংশ রয়েছে (যা হওয়া উচিত নয়)। তার ওপর, এই বেগুনি অংশগুলি দৃশ্যত বেশ চওড়া এবং মনে হচ্ছে সবগুলোই গ্রাফের সর্বোচ্চ চূড়া ছুঁয়েছে। আপনি আরও লক্ষ্য করবেন যে গ্রাফটি সবসময় পূর্ণ রয়েছে এবং বেগুনি অংশগুলি প্রায় কখনোই শূন্যে নেমে আসে না। মনে হচ্ছে ব্রাউজার সবসময় কিছু না কিছু করতে ব্যস্ত এবং প্রায় কখনোই বিশ্রাম নিচ্ছে না।

কিন্তু এই সমস্ত বেগুনি অংশের আসল অর্থ কী?

প্রোফাইলারটি সদয়ভাবে আমাদের জানিয়েছিল যে এগুলিকে রিফ্লো (reflows) বলা হয়। গুগল ডেভেলপার সাইট:

ডকুমেন্টে থাকা উপাদানগুলির অবস্থান এবং জ্যামিতি পুনরায় গণনা করার ওয়েব ব্রাউজার প্রক্রিয়ার নাম হলো রিফ্লো.. রিফ্লো হলো ব্রাউজারে একটি ইউজার-ব্লকিং অপারেশন।

সুতরাং এর অর্থ হলো PBS-এ মাউস ঘোরালে বিপুল পরিমাণে রিফ্লো শুরু হয়। সবচেয়ে খারাপ বিষয় হলো প্রতিটি রিফ্লো ব্রাউজারকে ক্ষণিকের জন্য থামিয়ে দেয় যাতে এটি তার গণনা শেষ করতে পারে। যত বেশি রিফ্লো, তত বেশি ব্রাউজার পজ, এবং ব্যবহারকারীর অভিজ্ঞতা আরও বেশি ঝাঁকুনিপূর্ণ (janky) হয়ে ওঠে। সহজ কথায়, প্রচুর বেগুনি এলাকা = খারাপ।

সুতরাং দেখা গেল অ্যান সঠিক ছিলেন: আমাদের সত্যিই একটি বিশাল পারফরম্যান্স সমস্যা রয়েছে, তাই আমাদের টিম অবিলম্বে এটি ঠিক করার কাজে নেমে পড়ল।

আমাদের রিফ্লো / জ্যাঙ্ক সমস্যার সমাধান করা

অন্য কিছুর আগে, ঠিক কী কারণে এই রিফ্লো ঘটে তা খুঁজে বের করার জন্য আমাদের কিছু গবেষণা করার প্রয়োজন ছিল। আমরা জানতে পেরেছি যে দৃশ্যত, জাভাস্ক্রিপ্টের অনেক সাধারণ বিষয় ব্রাউজারকে রিফ্লো ঘটাতে প্ররোচিত করতে পারে।

একটি DOM উপাদানের প্রস্থ, উচ্চতা বা অবস্থান পাওয়ার জন্য আপনি ব্যবহার করতে পারেন এমন প্রায় যেকোনো ফাংশনই রিফ্লো ট্রিগার করতে পারে। পল আইরিশ জাভাস্ক্রিপ্টের এমন সমস্ত প্রপার্টি এবং ফাংশনের একটি বিস্তৃত তালিকা লিখেছেন যা এটি ঘটাতে পারে। খারাপ খবর হলো পেজ বিল্ডার স্যান্ডউইচ এই ধরনের অনেক প্রপার্টি এবং ফাংশন ব্যবহার করে, যার প্রতিটিই রিফ্লো ঘটায়:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • এবং আরও অনেক কিছু…

সমাধান #১: ব্যয়বহুল রিফ্লো প্রক্রিয়াগুলি ক্যাশ করা

সবচেয়ে সুস্পষ্ট সমাধান হলো সেই প্রপার্টি এবং ফাংশনগুলি ব্যবহার না করা। তবে, PBS এগুলির ওপর ব্যাপকভাবে নির্ভরশীল এবং এগুলি ছাড়া আমরা কাজ চালাতে পারি না।

আমাদের কাছে অবশিষ্ট সমাধান ছিল সেই ফাংশনগুলিতে কলের সংখ্যা যতটা সম্ভব কমিয়ে আনা। এখানে কঠিন বিষয় ছিল যে আমরা PBS-এর বিভিন্ন অংশে এই প্রপার্টি এবং ফাংশনগুলি বহুবার ব্যবহার করেছি এবং বেশিরভাগ সময়ই আমরা একই DOM উপাদানে একাধিকবার সেগুলি ব্যবহার করেছি। এটি সমাধান করার জন্য, আমরা প্রতিটি সমস্যাযুক্ত প্রপার্টি/ফাংশনের জন্য আমাদের নিজস্ব ক্যাশিং মেকানিজম তৈরি করেছি।

উদাহরণস্বরূপ, getBoundingClientRect ফাংশনটি আমাদের একটি DOM উপাদানের বর্তমান top, left, width, height, right, এবং bottom পিক্সেল পরিমাপ প্রদান করে। 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 ব্যবহার করেছি। এখন প্রতিবার ক্লায়েন্ট রেক্ট কোয়েরি করা হলে, আমরা ভবিষ্যতের জন্য সেই মানটি সংরক্ষণ করে রাখি, যার ফলে রিফ্লো ট্রিগারের সংখ্যা সীমিত হয়।

সংরক্ষিত ক্লায়েন্ট রেক্টগুলি যখন অকার্যকর (invalidated) হয়ে যায়, তখন ক্যাশ মুছে ফেলার জন্য আমাদের একটি উপায়ের প্রয়োজন ছিল। ব্যবহারকারী যখন পৃষ্ঠা স্ক্রোল করেন বা স্ক্রিনে উপাদান যোগ/পরিবর্তন/অপসারণ করেন, তখন সংরক্ষিত ক্লায়েন্ট রেক্টের মান পরিবর্তিত হয়। তাই আমরা একটি ক্যাশ ক্লিয়ারিং ফাংশন তৈরি করেছি যা নতুন ক্লায়েন্ট রেক্ট মানের প্রয়োজন হলেই আমরা কল করতে পারি:

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

আমরা সাধারণত যে অন্যান্য ব্যয়বহুল ফাংশন কলগুলি ব্যবহার করি, সেগুলির জন্যও আমরা একই ক্যাশিং মেকানিজম প্রয়োগ করেছি।

সমাধান #২: FastDom ব্যবহার করে রিড এবং রাইটগুলি একসাথে গ্রুপ করা

সমাধান #১ বাস্তবায়নের ফলে PBS-এর পারফরম্যান্স বৃদ্ধি পেয়েছিল, তবে তা যথেষ্ট ছিল না।

আরও কিছুটা গুগল করে, আমরা ‘লেআউট থ্র্যাশিং’ প্রতিরোধ করার বিষয়ে উইলসন পেজের একটি নিবন্ধ দেখতে পেলাম। উইলসন বলেছেন:

লেআউট থ্র্যাশিং (রিফ্লো) তখনই ঘটে যখন জাভাস্ক্রিপ্ট তীব্রভাবে DOM-এ একাধিকবার রাইট করে, তারপর রিড করে, যার ফলে ডকুমেন্ট রিফ্লো ঘটে।

দৃশ্যত, নির্দিষ্ট রিফ্লো-ট্রিগারিং প্রপার্টি এবং ফাংশনগুলিতে আপনার কল সীমিত করাই যথেষ্ট নয়। আপনি যখন 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​

উপরের উদাহরণটি ৩টি রিফ্লো ট্রিগার করে কারণ একটি রাইটের পর পুরো পৃষ্ঠার মাত্রা (dimensions) বাতিল হয়ে যায়। এর জন্য প্রস্তাবিত সমাধান হলো কোডটিকে পুনর্বিন্যাস করে সমস্ত রিড এবং রাইট একসাথে গুচ্ছবদ্ধ করা যাতে পুরো বিষয়টি কেবল ১টি ব্রাউজার রিফ্লো ট্রিগার করে:

// 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-এর লাইফ-সাইকেল চলাকালীন অনেক ভিন্ন ভিন্ন সময়ে এবং অনেক ভিন্ন ভিন্ন জায়গায় রিফ্লো-ট্রিগারিং বিষয়গুলি রিড ও রাইট করা হয়। হাজার হাজার লাইনের জাভাস্ক্রিপ্ট কোড পুনর্গঠন করা সম্ভব নয়।

আমাদের সৌভাগ্যবশত, উইলসন তার নিবন্ধে একটি সমাধানও প্রদান করেছিলেন। তিনি সেই রিড এবং রাইটগুলি একসাথে গুচ্ছবদ্ধ করতে সাহায্য করার জন্য FastDom নামে একটি ছোট ইউটিলিটি স্ক্রিপ্ট তৈরি করেছিলেন।

পেজ বিল্ডার স্যান্ডউইচের কিছু কোড এখানে দেওয়া হলো যা FastDom ব্যবহার করে এবং আমাদের ওপরের সমাধান #১-ও অন্তর্ভুক্ত করে:

// 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 কলকে একসাথে গ্রুপ করে এবং সেগুলিকে অ্যাসিঙ্ক্রোনাসভাবে এক্সিকিউট করে, কার্যকরভাবে ব্রাউজারে রিফ্লোর পরিমাণ কমাতে সমস্ত রিড এবং রাইটকে একত্রিত করে।

সমাধান #৩: স্টাইল অবজেক্ট অ্যাক্সেস করা ব্যয়বহুল

আমরা 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;
}​

কোড উন্নতির ফলাফল

আমাদের উপরের ৩টি সমাধানের জন্য প্রচুর গবেষণা, পরীক্ষা এবং বাস্তব বাস্তবায়নের প্রয়োজন হয়েছিল (এবং আমাদের ফন্টস ফিচারটি প্রকাশে বিলম্ব ঘটিয়েছিল), তবে শেষ পর্যন্ত এটি সার্থক ছিল।

স্ক্রিন জুড়ে ২ সেকেন্ডের জন্য মাউস নাড়াচাড়া করার আমাদের প্রাথমিক পরীক্ষায় ফিরে যাই। এখানে পুরানো অসুন্দর/ঝাঁকুনিপূর্ণ টাইমলাইন দেওয়া হলো:

এবং পারফরম্যান্সের উন্নতির পর আমাদের নতুন টাইমলাইন দেখতে কেমন তা এখানে দেওয়া হলো:

যেমনটি আপনি দেখতে পাচ্ছেন, গ্রাফটিতে বেগুনি রঙের উপাদান কম রয়েছে। এবং ব্রাউজার কর্তৃক সম্পাদিত প্রক্রিয়ার পরিমাণে লক্ষণীয় পার্থক্য রয়েছে। নীচের আয়তক্ষেত্রাকার অংশগুলিও আগের চেয়ে পাতলা, যার অর্থ প্রক্রিয়াগুলি আগের চেয়ে দ্রুত সম্পন্ন হচ্ছে।

আপনি যদি পুরানো এবং নতুন গ্রাফের লম্বা/প্রশস্ত বেগুনি এলাকার পরিমাণের তুলনা করেন, তবে আমরা দেখতে পাবো এক বিশাল ২০০% পারফরম্যান্স বৃদ্ধি! আমরা সামগ্রিকভাবেও পারফরম্যান্সের উন্নতি দেখতে পাচ্ছি। মোটেও মন্দ নয়!

মূল শিক্ষণীয় বিষয়সমূহ

সত্যি বলতে, অ্যান বিষয়টি উল্লেখ করার আগে আমরা ভাবিনি যে PBS-এর পারফরম্যান্স সমস্যা রয়েছে। মূলত কারণ আমরা ডেভেলপমেন্ট-কেন্দ্রিক কম্পিউটার ব্যবহার করছিলাম এবং পারফরম্যান্সের বাধাগুলি ততটা স্পষ্ট ছিল না। পেছনের দিকে তাকালে মনে হয়, পারফরম্যান্স যাচাই করা শীর্ষ অগ্রাধিকারগুলির মধ্যে একটি হওয়া উচিত ছিল। তবে অন্তত এখন, সংস্করণ ৪.৩ অনুযায়ী এটির সমাধান করা হয়েছে। ?

যারা তাদের নিজস্ব ওয়ার্ডপ্রেস প্লাগইনের পারফরম্যান্স উন্নত করতে চান, বিশেষ করে যদি আপনিও ভারী জাভাস্ক্রিপ্ট ব্যবহার করেন, তাদের জন্য এই অভিজ্ঞতা থেকে আমার মূল শিক্ষণীয় বিষয়গুলি নিচে দেওয়া হলো:

  1. রিফ্লো ট্রিগার করে এমন প্রপার্টি/ফাংশনে আপনার কল সীমিত/ক্যাশ করুন। গাইড হিসেবে এটি ব্যবহার করুন: What forces layout/reflow. The comprehensive list.
  2. DOM স্টাইলে “reads” এবং “writes” একসাথে গ্রুপ করুন। আপনার হয়ে এটি করার জন্য FastDom ব্যবহার করুন।
  3. Chrome-এর Timeline profiler ব্যবহার করুন, এবং নিশ্চিত করুন যে আপনার যথাসম্ভব কম রিফ্লো (বেগুনি উপাদান) রয়েছে।