Как увеличихме производителността на нашия плъгин с 200%

Написано от

в

,

На 2 февруари 2017 г. Ан Тейлър написа статията 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right в CodeInWP. Ние непрекъснато подобряваме Page Builder Sandwich повече от година: правим проучвания, вслушваме се в клиентите си, затова смятахме, че Page Builder Sandwich ще влезе в статията на CodeInWP „Who Got it Right“. Но не успяхме поради проблеми с производителността. ?

Затова попитахме Ан за това и тя отговори:

…Поиграх си малко с безплатната версия. Изглежда много обещаващо: много модули, доста интуитивен потребителски интерфейс, страхотно WYSIWYG изживяване, всичко се мести с лекота чрез плъзгане. Въпреки това, когато на страницата имах около 10 модула, стана трудно да продължа работата си с него поради бавната скорост на редактиране…

И така, според нея Page Builder Sandwich има проблеми с производителността. И тъй като винаги се вслушваме в обратната връзка, решихме да проверим.

Тестване на производителността

Трябваше ни начин да проверим производителността на PBS; тъй като PBS работи предимно в браузъра чрез Javascript, профайлърът Timeline на Chrome беше правилният инструмент за тази задача. Профайлърът на времевата линия може да идентифицира и измерва кои процеси се изпълняват в уеб страница по време на изпълнение, което е идеално за откриване на тесни места в производителността.

Решихме първо да тестваме най-простите стъпки – движение на мишката върху множество елементи и редове. Просто, нали? Бяхме уверени, че няма да се сблъскаме с проблеми с производителността при нещо толкова просто. За съжаление, грешахме.

След като натиснахме бутона за запис, движихме мишката около 2 секунди по екрана, след което натиснахме бутона за спиране.  Ето получената времева линия от профайлъра:

И изненада! Пълно е с тесни места в производителността!

В графиката се случват много неща, но най-очевидният проблем е, че има много лилави зони (което не би трябвало да е така). Освен това тези лилави зони са видимо много широки и изглежда, че всички достигат върха на графиката. Също така ще забележите, че графиката е винаги пълна и че лилавите зони почти никога не спадат до нула. Изглежда сякаш браузърът постоянно е зает да прави нещо и почти никога не почива.

Но какво всъщност означават всички тези лилави зони?

Профайлърът любезно ни уведоми, че те се наричат reflows. Ето определението за това какво представлява reflow от сайта на Google Developers:

Reflow е името на процеса в уеб браузъра за преизчисляване на позициите и геометрията на елементите в документа.. reflow е операция, блокираща потребителя в браузъра.

Това означава, че движението на мишката в PBS задейства тон reflow-та. Още по-лошото е, че всеки reflow временно спира браузъра, за да може той да завърши изчисленията си. Повече reflow-та означават повече паузи в браузъра, а изживяването става още по-накъсано. Казано по-просто, много лилави зони = лошо.

И така, оказа се, че Ан е била права: ние наистина имаме огромно тясно място в производителността, така че нашият екип веднага се залови за работа, за да го отстрани незабавно.

Отстраняване на проблема с Reflow / накъсването

Преди всичко трябваше да направим проучване, за да разберем какво изобщо причинява този reflow. Установихме, че очевидно много често срещани неща в Javascript могат да накарат браузъра да предизвика reflow.

Почти всяка функция, която можете да използвате за извличане на ширината, височината или позицията на DOM елемент, може да задейства reflow. Пол Айриш е написал изчерпателен списък с всички свойства и функции в Javascript, които могат да направят това. Лошата новина е, че Page Builder Sandwich използва много от тези свойства и функции, като всички те причиняват reflow:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • и още много други…

Решение №1: Кеширане на ресурсоемките Reflow процеси

Най-очевидното решение е да НЕ използваме тези свойства и функции. Въпреки това PBS силно разчита на тях и не можем да се справим без тях.

Решението, което ни остана, е да ограничим максимално извикванията към тези функции. Трудната част тук беше, че използвахме тези свойства и функции многократно в различни части на PBS, като през повечето време ги използвахме върху един и същ DOM елемент по няколко пъти. За да решим това, създадохме собствен механизъм за кеширане за всяко проблематично свойство/функция.

Например функцията getBoundingClientRect  ни дава текущите измервания в пиксели за top, left, width, height, right и bottom на даден DOM елемент. Използваме тази функция постоянно, за да изчислим местоположението, където да очертаем контурите на елементите в PBS. Ето функцията за кеширане, която създадохме за това:

// Кеш: Нашите client rects се пазят тук, можем да използваме това, за да ги изчистим по-късно.
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// Проверка дали вече сме взели client rect преди.
	if ( ! element._boundingClientRect ) {

		// Ако не, взимаме го и го съхраняваме за бъдеща употреба.
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

Вместо да извикваме getBoundingClientRect , вече използваме pbsGetBoundingClientRect . Сега всеки път, когато се направи заявка за client rect, съхраняваме тази стойност за бъдеща справка, ограничавайки по този начин броя на задействанията на reflow.

Също така ни беше необходим начин да изчистим кеша, когато съхранените client rects станат невалидни. Стойностите на съхранения client rect се променят, когато потребителят превърта страницата или добавя/променя/премахва елементи на екрана. Затова създадохме функция за изчистване на кеша, която можем да извикваме винаги, когато ни трябват пресни стойности на client rect:

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

Приложихме същия механизъм за кеширане и за други ресурсоемки извиквания на функции, които обикновено използваме.

Решение №2: Групиране на операциите за четене и запис чрез FastDom

Внедряването на решение №1 подобри производителността на PBS, но не беше достатъчно.

Търсейки още в Google, попаднахме на статия от Уилсън Пейдж за предотвратяването на „layout thrashing“. Уилсън казва:

Layout Thrashing (reflow) възниква, когато JavaScript интензивно записва, а след това чете от DOM неколкократно, причинявайки преизчислявания на оформлението на документа (reflows).

Очевидно не е достатъчно само да ограничите извикванията си до определени свойства и функции, задействащи reflow. Когато извършвате множество последователни операции за четене („reads“) и запис („writes“) в DOM, това също задейства reflow в браузъра. Ето един типичен сценарий, директно от сайта на Уилсън:

// Четене
var h1 = element1.clientHeight;

// Запис (инвалидира оформлението)
element1.style.height = (h1 * 2) + 'px';

// Четене (задейства reflow)
var h2 = element2.clientHeight;

// Запис (инвалидира оформлението)
element2.style.height = (h2 * 2) + 'px';

// Четене (задейства reflow)
var h3 = element3.clientHeight;

// Запис (инвалидира оформлението)
element3.style.height = (h3 * 2) + 'px';

// Документът прави reflow в края на кадъра​

Горният пример задейства 3 reflow-та, тъй като размерите на цялата страница се инвалидират след запис. Предложеното решение за това е кодът да се пренареди така, че да групира всички четения и записи заедно, така че цялото нещо да задейства само 1 reflow в браузъра:

// Четене
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Запис (инвалидира оформлението)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Документът прави reflow в края на кадъра​

Горният откъс прави отстраняването на проблема да изглежда лесно. Но в действителност, в PBS четенето и записването на неща, задействащи reflow, се случва на много различни места и по много различно време по време на жизнения цикъл на PBS. Реорганизирането на хиляди редове Javascript код не е осъществимо.

За наше щастие, Уилсън предостави решение и в своята статия. Той създаде малък помощен скрипт, наречен FastDom, който помага за групирането на тези четения и записи.

Ето малко код от Page Builder Sandwich, който използва FastDom и също така включва нашето Решение №1 от по-горе:

// Групиране на четенията от други нишки.
fastdom.measure( function() {

	// Използване на нашия кеширан client rect.
	var rect = window.pbsGetBoundingClientRect( elem );

	// Групиране на записите от други нишки.
	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-та в браузъра.

Решение №3: Достъпът до обекти със стилове е ресурсоемък

Също така открихме с помощта на jsPerf, че някои функции за извличане на стилове са по-бързи от други.

В PBS обикновено трябва да вземем текущата стойност на определен стил на даден елемент. Например обикновено използваме това, за да вземем горното поле (top margin):

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

Но открихме, че правенето на следното е много по-бързо:

var marginTop = elem.style.marginTop;​

Въпреки това elem.style.marginTop  и getComputedStyle  са много различни. Първото ни дава само вградения (inline) стил на елемента и не взема предвид никакви таблици със стилове (stylesheets); а второто взема предвид както вградените стилове, така и таблиците със стилове.

Нещо повече, дори когато резултатът от getComputedStyle  беше кеширан, той все още беше наистина бавен. Оказва се, че самият достъп до обекта със стилове е бавният процес.

За наше щастие PBS добавя вградени (inline) стилове към елементите и тъй като вградените стилове имат предимство (през повечето време) пред таблиците със стилове, ще има случаи, в които получаването на стойността на вградения стил ще бъде достатъчно. Така че, ако комбинираме двата метода по-горе, можем да получим код, който работи много по-бързо:

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

Резултатите от подобренията в кода

Трите ни решения по-горе изискваха много проучвания, тестване и реална реализация (и забавиха пускането на функцията ни за шрифтове Fonts), но в крайна сметка всичко си заслужаваше.

Връщайки се към първоначалния ни тест с движение на мишката по екрана за 2 секунди. Ето старата грозна/накъсваща времева линия:

А ето как изглежда новата ни времева линия след подобренията в производителността:

Както виждате, в графиката има по-малко лилави елементи. Има и забележима разлика в количеството процеси, изпълнявани от браузъра. Правоъгълните елементи в долната част също са по-тънки, което означава, че процесите се изпълняват по-бързо от преди.

Ако сравните количеството високи/широки лилави зони в старата и новата графика, наблюдаваме изумително увеличение на производителността с 200%! Също така виждаме подобрения в производителността навсякъде. Никак не е зле!

Изводи и научени уроци

Честно казано, не смятахме, че PBS има проблеми с производителността, преди Ан да повдигне въпроса. Главно защото използвахме машини, ориентирани към разработка, и тесните места в производителността не бяха толкова очевидни. Поглеждайки назад, проверката на производителността трябваше да бъде един от основните приоритети. Но поне сега, от версия 4.3 нататък, това е оправено. ?

За тези, които искат да подобрят производителността на собствените си WordPress разширения (плъгини), особено ако използвате интензивен Javascript, ето моите изводи от този опит:

  1. Ограничете/кеширайте извикванията си към свойства/функции, които задействат reflow. Използвайте това като ръководство: What forces layout/reflow. The comprehensive list.
  2. Групирайте заедно операциите за четене („reads“) и запис („writes“) на DOM стилове. Използвайте FastDom, за да направи това вместо вас.
  3. Използвайте профайлъра Timeline на Chrome и се уверете, че имате възможно най-малко reflow-та (лилави неща).