چگونه عملکرد افزونه خود را ۲۰۰٪ افزایش دادیم

نوشته شده توسط

در

,

در ۲ فوریه ۲۰۱۷، ان تیلور مقاله‌ای با عنوان ۱۲ چیزی که باید از صفحه‌سازهای وردپرس در سال ۲۰۱۷ ببینیم + چه کسانی از قبل آن را درست انجام داده‌اند در CodeInWP نوشت. ما بیش از یک سال بود که به طور مداوم در حال بهبود Page Builder Sandwich بودیم: تحقیق می‌کردیم، به مشتریانمان گوش می‌دادیم، بنابراین فکر می‌کردیم Page Builder Sandwich قرار است به مقاله «چه کسانی آن را درست انجام داده‌اند» CodeInWP راه پیدا کند. اما به دلیل عملکرد، این اتفاق نیفتاد. ?

بنابراین از آن در این باره پرسیدیم، و او پاسخ داد:

…من کمی با نسخه رایگان کار کردم. بسیار نویدبخش است: ماژول‌های زیاد، رابط کاربری بسیار شهودی، تجربه WYSIWYG عالی، همه چیز به راحتی کشیده و رها می‌شود. با این حال، وقتی حدود ۱۰ ماژول در صفحه داشتم، به دلیل سرعت پایین ویرایش، ادامه کار با آن دشوار شد…

بنابراین به گفته او، Page Builder Sandwich مشکلات عملکردی دارد. و از آنجایی که ما همیشه به بازخوردها گوش می‌دهیم، آن را بررسی کردیم.

آزمایش عملکرد

ما به روشی برای بررسی عملکرد PBS نیاز داشتیم؛ از آنجا که PBS بیشتر از طریق جاوا اسکریپت روی مرورگر اجرا می‌شود، پروفایلر تایم‌لاین کروم ابزار مناسبی برای این کار بود. پروفایلر تایم‌لاین می‌تواند فرآیندهایی را که در طول زمان اجرا در یک صفحه وب رخ می‌دهند شناسایی و اندازه‌گیری کند، که برای پیدا کردن گلوگاه‌های سرعت عالی است.

فکر کردیم ابتدا باید ساده‌ترین مراحل را آزمایش کنیم – حرکت دادن ماوس روی چندین المان و ردیف. ساده است، درست است؟ مطمئن بودیم که با چیزی به این سادگی با مشکل عملکردی مواجه نخواهیم شد. متأسفانه اشتباه می‌کردیم.

پس از زدن دکمه ضبط، ماوس را حدود ۲ ثانیه روی صفحه حرکت دادیم، سپس دکمه توقف را زدیم.  این تایم‌لاین حاصل از پروفایلر است:

و اینک شگفتی! پر از گلوگاه‌های عملکردی است!

چیزهای زیادی در نمودار در حال رخ دادن است، اما واضح‌ترین مشکل این است که بخش‌های بنفش زیادی در آن وجود دارد (که نباید این‌طور باشد). علاوه بر این، این بخش‌های بنفش به طور مشهودی بسیار پهن هستند و به نظر می‌رسد همگی به اوج نمودار می‌رسند. همچنین متوجه خواهید شد که نمودار همیشه پر است و بخش‌های بنفش تقریباً هرگز به صفر نمی‌رسند. به نظر می‌رسد که مرورگر همیشه مشغول انجام کاری است و تقریباً هرگز استراحت نمی‌کند.

اما تمام این بخش‌های بنفش واقعاً چه معنایی دارند؟

پروفایلر لطف کرد و به ما گفت که آنها reflow (تغییر چیدمان) نامیده می‌شوند. در اینجا تعریف reflow از سایت توسعه‌دهندگان گوگل:

Reflow نام فرآیند مرورگر وب برای محاسبه مجدد موقعیت‌ها و هندسه المان‌ها در سند است.. reflow یک عملیات مسدودکننده کاربر در مرورگر است.

این یعنی حرکت دادن ماوس در PBS باعث ایجاد تعداد بسیار زیادی reflow می‌شود. آنچه بدتر است این است که هر reflow مرورگر را برای لحظه‌ای متوقف می‌کند تا بتواند محاسبات خود را به پایان برساند. reflowهای بیشتر یعنی توقف‌های بیشتر مرورگر، و تجربه کاربری حتی پر از لگ و کندی می‌شود. به زبان ساده‌تر، بخش‌های بنفش زیاد = بد.

بنابراین مشخص شد که حق با آن بود: ما واقعاً یک گلوگاه عملکردی بزرگ داریم، بنابراین تیم ما بلافاصله دست به کار شد تا فوراً آن را برطرف کند.

حل مشکل Reflow / کندی ما

قبل از هر چیز، نیاز به کمی تحقیق داشتیم تا متوجه شویم در وهله اول چه چیزی باعث این reflow می‌شود. ما متوجه شدیم که ظاهراً، بسیاری از موارد رایج در جاوا اسکریپت می‌توانند مرورگر را وادار به ایجاد reflow کنند.

تقریباً هر تابعی که بتوانید برای دریافت عرض، ارتفاع یا موقعیت یک المان DOM استفاده کنید، می‌تواند یک reflow ایجاد کند. پائول آیریش فهرست کاملی از تمام ویژگی‌ها و توابعی در جاوا اسکریپت که می‌توانند این کار را انجام دهند، نوشته است. خبر بد این است که Page Builder Sandwich از بسیاری از این ویژگی‌ها و توابع استفاده می‌کند که همگی باعث reflow می‌شوند:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • و موارد بسیار بیشتر…

راه‌حل شماره ۱: کش کردن فرآیندهای سنگین Reflow

واضح‌ترین راه‌حل این است که از این ویژگی‌ها و توابع استفاده نکنیم. با این حال، 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 استفاده کردیم. حالا هر بار که مستطیل المان مورد پرس‌وجو قرار می‌گیرد، ما آن مقدار را برای ارجاع بعدی ذخیره می‌کنیم و در نتیجه تعداد محرک‌های reflow را محدود می‌سازیم.

ما همچنین به راهی برای پاک کردن کش در زمانی که مقادیر ذخیره شده نامعتبر می‌شوند، نیاز داشتیم. مقادیر مستطیل ذخیره شده زمانی تغییر می‌کند که کاربر صفحه را اسکرول کند یا المان‌هایی را به صفحه اضافه/ویرایش/حذف نماید. بنابراین ما یک تابع پاکسازی کش ایجاد کردیم که می‌توانیم هر زمان که به مقادیر جدید مستطیل المان نیاز داشتیم، آن را فراخوانی کنیم:

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

ما همچنین همین مکانیسم کش را برای سایر فراخوانی‌های توابع سنگین که معمولاً استفاده می‌کنیم، پیاده‌سازی کردیم.

راه‌حل شماره ۲: گروه‌بندی خواندن‌ها و نوشتن‌ها در کنار هم با استفاده از FastDom

اجرای راه‌حل شماره ۱ عملکرد PBS را افزایش داد، اما کافی نبود.

با کمی جستجوی بیشتر در گوگل، به مقاله‌ای از ویلسون پیج درباره جلوگیری از کوبش چیدمان (Layout Thrashing) برخوردیم. ویلسون می‌گوید:

کوبش چیدمان (reflow) زمانی اتفاق می‌افتد که جاوا اسکریپت به شدت و چندین بار متوالی در DOM می‌نویسد و سپس از آن می‌خواند که باعث reflowهای مکرر در سند می‌شود.

ظاهراً، محدود کردن فراخوانی‌ها به ویژگی‌ها و توابع خاص محرک reflow کافی نیست. وقتی چندین «خواندن» و «نوشتن» متوالی روی DOM انجام می‌دهید، این کار نیز یک reflow مرورگر را فعال می‌کند. در اینجا یک سناریوی معمول آورده شده است، برگرفته مستقیم از سایت ویلسون:

// 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​

مثال بالا ۳ بار reflow ایجاد می‌کند زیرا ابعاد کل صفحه بعد از هر نوشتن نامعتبر می‌شود. راه‌حل پیشنهادی برای این کار این است که کد را دوباره مرتب کنید تا همه خواندن‌ها و نوشتن‌ها را با هم گروه‌بندی کنید تا کل این فرآیند فقط ۱ بار reflow مرورگر ایجاد کند:

// 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، خواندن و نوشتن موارد محرک reflow در جاهای بسیار مختلف و در زمان‌های بسیار متفاوتی در چرخه حیات PBS رخ می‌دهد. سازماندهی مجدد هزاران خط کد جاوا اسکریپت امکان‌پذیر نیست.

خوشبختانه برای ما، ویلسون در مقاله خود یک راه‌حل نیز ارائه داد. او یک اسکریپت کاربردی کوچک به نام FastDom ساخت تا به گروه‌بندی این خواندن‌ها و نوشتن‌ها کمک کند.

در اینجا کدی از Page Builder Sandwich آمده است که از 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 را گروه‌بندی کرده و آنها را به صورت ناهمگام اجرا می‌کند، که به طور مؤثری همه خواندن‌ها و نوشتن‌ها را با هم دسته‌بندی می‌کند تا تمام reflowها را در مرورگر کاهش دهد.

راه‌حل شماره ۳: دسترسی به اشیاء Style هزینه‌بر است

ما همچنین با کمک 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. فراخوانی‌های خود را به ویژگی‌ها/توابعی که reflow ایجاد می‌کنند محدود/کش کنید. از این به عنوان راهنما استفاده کنید: چه چیزی باعث layout/reflow اجباری می‌شود. فهرست جامع.
  2. «خواندن‌ها» و «نوشتن‌ها» در استایل‌های DOM را با هم گروه‌بندی کنید. از FastDom استفاده کنید تا این کار را برای شما انجام دهد.
  3. از پروفایلر تایم‌لاین کروم استفاده کنید و مطمئن شوید که تا حد ممکن reflow (موارد بنفش) کمی دارید.