Как мы повысили производительность нашего плагина на 200%

Написано

в

,

2 февраля 2017 года Энн Тейлор опубликовала на CodeInWP статью 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right («12 вещей, которые мы хотим видеть в конструкторах страниц WordPress в 2017 году + кто уже делает все правильно»). Мы непрерывно улучшали Page Builder Sandwich больше года: проводили исследования, прислушивались к клиентам, поэтому были уверены, что Page Builder Sandwich попадет в раздел «Кто уже делает все правильно» на CodeInWP. Однако мы туда не попали из-за производительности. ?

Мы спросили Энн об этом, и она ответила:

…Я немного поработала с бесплатной версией. Она выглядит очень многообещающе: много модулей, довольно интуитивный интерфейс, отличный WYSIWYG, все легко перетаскивается. Однако, когда на странице появилось около 10 модулей, продолжать работу стало трудно из-за низкой скорости редактирования…

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

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

Нам нужен был способ проверить производительность PBS; поскольку PBS в основном работает в браузере с помощью Javascript, подходящим инструментом стал профилировщик Timeline в Chrome. Профилировщик Timeline может определять и измерять процессы, происходящие на веб-странице во время выполнения, что идеально подходит для поиска узких мест производительности.

Мы решили, что сначала нужно протестировать самые простые действия — перемещение курсора мыши по нескольким элементам и строкам. Просто, правда? Мы были уверены, что не столкнемся с проблемами производительности при чем-то настолько простом. К сожалению, мы ошибались.

Нажав кнопку записи, мы двигали мышь по экрану около 2 секунд, а затем нажали кнопку остановки.  Вот получившаяся временная шкала из профилировщика:

И что бы вы думали? Она под завязку забита узкими местами производительности!

На графике происходит очень много всего, но самая очевидная проблема заключается в обилии фиолетовых областей (которых быть не должно). Кроме того, эти фиолетовые области заметно широкие и, кажется, все достигают пика графика. Вы также заметите, что график постоянно заполнен, а фиолетовые области почти никогда не опускаются до нуля. Создается впечатление, что браузер постоянно чем-то занят и почти никогда не отдыхает.

Но что же на самом деле означают все эти фиолетовые области?

Профилировщик любезно подсказал нам, что они называются reflow (перекомпоновка). Вот определение того, что такое reflow, с сайта Google Developers:

Reflow — это название процесса веб-браузера по перерасчету позиций и геометрии элементов в документе… reflow — это блокирующая пользователя операция в браузере.

Это означает, что перемещение мыши в PBS вызывает кучу reflow. Хуже того, каждый reflow на мгновение приостанавливает работу браузера, чтобы он мог завершить свои вычисления. Больше reflow означает больше пауз браузера, а интерфейс становится еще более дерганым (janky). Проще говоря, много фиолетовых областей = плохо.

Так что Энн оказалась права: у нас действительно есть огромная проблема с производительностью, поэтому наша команда немедленно принялась за работу, чтобы ее устранить.

Устранение проблемы с Reflow / рывками

Прежде всего, нам нужно было провести небольшое исследование, чтобы выяснить, что вообще вызывает этот reflow. Мы выяснили, что, оказывается, множество привычных вещей в Javascript могут провоцировать браузер на выполнение reflow.

Практически любая функция, которую вы можете использовать для получения ширины, высоты или положения DOM-элемента, способна вызвать reflow. Пол Айриш составил подробный список всех свойств и функций в Javascript, приводящих к этому. Плохая новость заключается в том, что Page Builder Sandwich использует множество этих свойств и функций, и все они вызывают reflow:

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

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

Самое очевидное решение — НЕ использовать эти свойства и функции. Однако 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 . Теперь каждый раз при запросе client rect мы сохраняем это значение для последующего использования, тем самым ограничивая количество триггеров reflow.

Нам также понадобился способ очистки кэша при инвалидации сохраненных значений client rect. Значения сохраненных 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 множество раз подряд, вызывая множественные перекомпоновки документа.

Судя по всему, недостаточно просто ограничить вызовы определенных свойств и функций, вызывающих reflow. Выполнение нескольких последовательных операций чтения («read») и записи («write») в 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​

Приведенный выше пример вызывает 3 reflow, поскольку размеры всей страницы инвалидируются после каждой записи. Предлагаемое решение — изменить порядок кода, чтобы объединить все операции чтения и записи вместе, благодаря чему вся цепочка вызовет только 1 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, происходят во множестве разных мест и в разное время жизненного цикла приложения. Переписать тысячи строк кода Javascript просто нереально.

К счастью для нас, Уилсон также предложил решение в своей статье. Он создал небольшую вспомогательную библиотеку для пакетной обработки таких операций чтения и записи под названием 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  вместе и выполняет их асинхронно, эффективно группируя все операции чтения и записи, чтобы сократить количество reflow в браузере.

Решение №3: Обращение к объектам стилей обходится дорого

С помощью jsPerf мы также выяснили, что некоторые функции получения стилей работают быстрее других.

В PBS нам часто требуется получать текущее значение определенного стиля элемента. Например, обычно мы использовали следующее для получения верхнего отступа (margin-top):

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;
}​

Результаты оптимизации кода

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

Возвращаясь к нашему первоначальному тесту с перемещением курсора мыши по экрану в течение 2 секунд. Вот старая неприглядная временная шкала с рывками:

А вот как выглядит наша новая шкала после улучшений производительности:

Как видите, фиолетовых элементов на графике стало меньше. Разница в количестве процессов, выполняемых браузером, очевидна. Прямоугольные блоки внизу также стали тоньше, что говорит о более быстром выполнении процессов, чем раньше.

Если сравнить количество высоких/широких фиолетовых областей на старом и новом графиках, перед нами колоссальный прирост производительности в 200%! Мы также наблюдаем общее увеличение быстродействия во всех аспектах. Совсем неплохо!

Выводы и усвоенные уроки

Честно говоря, мы не предполагали, что у PBS есть проблемы с производительностью, пока об этом не упомянула Энн. Главным образом потому, что мы работали на мощных компьютерах для разработки, и узкие места были не так заметны. Оглядываясь назад, проверка производительности должна была стать одной из приоритетных задач. Но, по крайней мере, начиная с версии 4.3 этот вопрос решен. ?

Для тех, кто хочет повысить производительность своего плагина для WordPress, особенно если вы активно используете Javascript, вот мои основные выводы из этого опыта:

  1. Ограничивайте/кэшируйте обращения к свойствам и функциям, вызывающим reflow. Используйте в качестве руководства список: What forces layout/reflow. The comprehensive list.
  2. Группируйте операции чтения («read») и записи («write») для стилей DOM. Используйте FastDom, чтобы автоматизировать это.
  3. Используйте профилировщик Timeline в Chrome и следите за тем, чтобы у вас было как можно меньше reflow (фиолетовых зон).