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, профільовщик Chrome Timeline був ідеальним інструментом для цього завдання. Профільовщик Timeline дозволяє визначити та виміряти процеси, що відбуваються на вебсторінці під час її роботи, що чудово підходить для пошуку вузьких місць у швидкодії.
Ми вирішили спершу протестувати найпростіші дії — провести мишею над кількома елементами та рядками. Просто, чи не так? Ми були впевнені, що у чомусь настільки простому не виникне проблем із продуктивністю. На жаль, ми помилялися.
Щойно ми натиснули кнопку запису, ми поводили мишею по екрану близько 2 секунд, а потім натиснули кнопку зупинки. Ось отриманий графік Timeline із профільовщика:
І що ж ви думаєте? Він переповнений вузькими місцями продуктивності!
На графіку відбувається багато всього, але найбільш очевидна проблема полягає в тому, що на ньому дуже багато фіолетових ділянок (чого бути не повинно). До того ж ці фіолетові ділянки помітно дуже широкі, і всі вони, схоже, досягають піку графіка. Ви також помітите, що графік постійно заповнений, а фіолетові ділянки практично ніколи не опускаються до нуля. Схоже, що браузер постійно чимось зайнятий і майже ніколи не відпочиває.
Але що насправді означають усі ці фіолетові ділянки?
Профільовщик люб’язно підказав нам, що вони називаються reflows. Ось визначення того, що таке reflow, із сайту Google Developer:
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. Ось функція кешування, яку ми для цього створили:
// Кеш: наші прямокутники клієнта зберігаються тут, ми можемо використати це для їх очищення пізніше.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Перевіряємо, чи ми вже отримували прямокутник клієнта раніше.
if ( ! element._boundingClientRect ) {
// Якщо ні, отримуємо його та зберігаємо для подальшого використання.
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 = [];
};
Ми також реалізували такий самий механізм кешування для інших ресурсомістких викликів функцій, якими зазвичай користуємося.
Виправлення №2: Групування операцій читання та запису за допомогою FastDom
Впровадження виправлення №1 підвищило продуктивність PBS, але цього було недостатньо.
Пошукавши ще трохи в Google, ми натрапили на статтю Вілсона Пейджа про запобігання «layout thrashing». Вілсон пише:
Layout Thrashing (reflow) виникає, коли JavaScript інтенсивно записує, а потім читає з DOM кілька разів поспіль, викликаючи повторні reflow документа.
Як виявилося, недостатньо просто обмежити виклики конкретних властивостей і функцій, що провокують reflow. Коли ви послідовно виконуєте кілька операцій «читання» та «запису» в 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() {
// Використовуємо наш закешований прямокутник клієнта.
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 нам зазвичай потрібно отримати поточне значення певного стилю елемента. Наприклад, зазвичай ми використовуємо це для отримання верхнього margin:
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 виправлення вище вимагали багато досліджень, тестування та практичної реалізації (і відтермінували реліз нашої функції «Шрифти»), але зрештою все це було того варте.
Повернімося до нашого початкового тесту з рухом мишею по екрану протягом 2 секунд. Ось старий непривабливий графік із затримками:
А ось як виглядає наш новий графік після покращення продуктивності:
Як бачите, фіолетових областей на графіку стало значно менше. І є помітна різниця в кількості процесів, які виконує браузер. Прямокутні елементи внизу також тонші, що означає, що процеси виконуються швидше, ніж раніше.
Якщо порівняти кількість високих/широких фіолетових областей на старому та новому графіках, ми бачимо колосальне зростання продуктивності на 200%! Ми також спостерігаємо загальне підвищення швидкодії за всіма показниками. Зовсім непогано!
Висновки та засвоєні уроки
Чесно кажучи, ми не думали, що PBS має проблеми з продуктивністю, доки про це не сказала Енн. Головним чином тому, що ми використовували потужні машини для розробки, і вузькі місця у продуктивності були не настільки помітними. Озираючись назад, перевірка продуктивності мала б бути одним із головних пріоритетів. Але принаймні зараз, починаючи з версії 4.3, про це подбали. 
Для тих, хто хоче покращити продуктивність власного плагіна для WordPress, особливо якщо ви також активно використовуєте Javascript, ось мої висновки з цього досвіду:
- Обмежуйте/кешуйте виклики до властивостей/функцій, які провокують reflow. Використовуйте це як посібник: What forces layout/reflow. The comprehensive list.
- Групуйте разом операції «читання» та «запису» до стилів DOM. Використовуйте FastDom, щоб робити це автоматично.
- Використовуйте профільовщик Chrome Timeline і стежте, щоб у вас було якомога менше reflow (фіолетових ділянок).