Профилирование JavaScript с Chrome Developer Tools

в 22:22, , рубрики: Google Chrome, google chrome developer tools, javascript, profiling, Веб-разработка

Скорость сайта состоит из 2 частей: как быстро загружается страница и как быстро работает код в ней. Многие сервисы помогают ускорить загрузку, такие как минификаторы или CDN, то скорость работы кода зависит только от вас.

Небольшие изменения в коде могут давать огромные изменения в производительности. Всего несколько строк могут означать разницу между быстрым сайтом и диалогом “Unresponsive Script”.

Установите точку отсчёта

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

Профилирование JavaScript с Chrome Developer Tools

Страница загружается достаточно быстро, но всё равно необходимо некоторые время и есть небольшое моргание перед тем, как закончится прорисовка. Пора заняться профилированием и сделать её быстрее.
Всегда начинайте ускорение производительности с установки точки отсчёта, чтобы знать насколько быстрое или медленное приложение в данный момент. Точка отчёта даст понимание, действительно ли вы ускоряете программу и поможет делать компромисы.

Профайлер является частью Chrome Developer Tools, который всегда доступен в Chrome. Нажмите Ctrl+Shift+I, чтобы запустить его. В Chrome ещё есть отличная утилита для отслеживания событий – Speed Tracer.

Для определения нашей точки отсчёта, начнём запись во вкладке “Timeline”, загрузим страницу и остановим запись (для старта записи в открытом Chrome Developer Tools перейдите на вкладку “Timeline” и в нижнем левом углу нажмите иконку – чёрный круг – “Record”). Chrome достаточно умный, чтобы не начинать запись пока страница не начнёт загружаться. Я запустил её 3 раза и посчитал среднее значение.

Профилирование JavaScript с Chrome Developer Tools

Моя точка отсчёта – время между первым запросом страницы и окончанием прорисовки страницы в браузере – 1,25 секунды. Это неплохо, но не так и хорошо для такой простой страницы.

Я хочу, чтобы код работал быстрее, но не знаю, что именно его замедляет. Профайлер должен помочь найти причину.

Создание профиля

Timeline показывает время работы кода, но не даёт информации по работе частей кода. Мы можем что-то менять и запускать timeline снова и снова, но это как стрелять по воробьям. Вкладка “Profiles” даёт более удобный способ увидеть, что происходит.

Профайлеры показывают, какие функции занимают больше всего времени. Давайте сделаем первоначальный профиль, переключившись на вкладку “Profiles”, где есть три типа профилирования:

  1. JavaScript CPU profile
    Показывает, сколько времени процессора занимает наш JavaScript.
  2. CSS selector profile
    Показывает сколько времени CPU занимает обработка CSS-селекторов
  3. Heap snapshot
    Показывает, сколько памяти используют наши переменные в JavaScript

Мы хотим сделать JavaScript быстрее, поэтому будем использовать CPU profiling. Для старта профайлера, обновите страницу и затем остановите профайлер.

Профилирование JavaScript с Chrome Developer Tools

Видно, что происходит много вещей. Сортировщик цветов использует jQuery и jQuery UI, которые делают много вещей вроде управления плагинами и парсинга регулярных выражений. Также видно, что 2 из моих функций вверху списка: decimalToHex и makeColorSorder. Эти 2 функции в сумме занимают 13,2% времени процессора, поэтому это удачное место для начала улусшений.

Можно нажать на стрелку рядом с функцией для отображения полного стека вызова функции. В нашем случае, я вижу, что decimalToHex вызывается из makeColorSorter, а makeColorSorter вызывается из $(document).ready.

Вот код:

$(document).ready(function() {
    makeColorSorter(.05, .05, .05, 0, 2, 4, 128, 127, 121);
    makeSortable();
});

Понимание, откуда они вызываются, также даёт понимание, что делание цветов перетягиваемыми не самая большая проблема производительности. В данном случае больше времени уходит на добавление DOM-элементов, чем на обеспечение возможности их перетягивать.

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

Изолирование проблемы

Вместо загрузки сортировщика цветов после загрузки документа, я сделаю вторую версию, которая ждёт, пока я нажму на кнопку. Это уменьшает влияние других вещей, которые делает браузер, и позволяет профилировать только код. Я верну всё обратно, после того как ускорю производительность.

Давайте назовём новую функцию testColorSorter и привяжем её к кнопке:

function testColorSorter() {
    makeColorSorter(.05, .05, .05, 0, 2, 4, 128, 127, 121);
    makeSortable();
}
//<button id="clickMe" onclick="testColorSorter();">Click me</button>

Изменение приложения могло непредсказуемо повлиять на производительность. Это изменение выглядит довольно безопасным, но я всё же хочу запустить профайлер ещё раз, чтобы проверить, не повлиял ли я на что-то ещё. Я создам новый профиль, запустив профайлер, нажав кнопку в приложении и остановив профайлер.

Профилирование JavaScript с Chrome Developer Tools

Первая вещь, на которую надо обратить внимание, это функция decimalToHex, которая теперь занимает 4,23% от времени загрузки; это то, что в коде работает дольше всего. Давайте создадим новую точку отсчёта, чтобы посмотреть как улучшится код в данном сценарии.

Профилирование JavaScript с Chrome Developer Tools

Несколько событий происходят до нажатия кнопки, но меня беспокоит только время между нажатием кнопки и отрисовкой сортировщика цветов. Кнопка нажата на 390-й миллисекунде, а событие Paint случилось на 726-й миллисекунде; 726 минус 290 это точка отсчёта – 390 миллисекунд. Так же как и в первый раз, я запустил три раза и посчитал среднее значение.

Сейчас я знаю куда смотреть и время работы кода. Мы готовы чинить проблему.

Сделать быстрее

Профайлер только говорит, какая функция приводит к проблеме, поэтому нам надо посмотреть внутрь неё и понять, что она делает.

function decimalToHex(d) {
    var hex = Number(d).toString(16);
    hex = "00".substr(0, 2 - hex.length) + hex;
    console.log('converting ' + d + ' to ' + hex);
    return hex;
}

Каждая точка в сортировщике цветов берёт значение цвета фона в 16-ричном формате вроде #86F01B или #2456FE. Функция decimalToHex превращает значения RGB в hex-цвета, которые мы можем использовать на странице.

Функция довольно простая, но я оставил console.log там, который можно удалить. Она также добавляет отступ в начало цвета. Это важно, потому что некоторые 10-ричные числа могут привести к односимвольному 16-ричному числу; например 10-ричное “12” – это 16-ричное “C”, а CSS требует 2 символа. Мы можем сделать это преобразование немного менее общим.

function decimalToHex(d) {
    var hex = Number(d).toString(16);
    return hex.length === 1 ? '0' + hex : hex;
}

Версия 3 сортировщика цветов меняет строку только, когда она требует отступа и не вызывает substr. С этой новой функцией, код выполняется за 137 миллисекунд. Профилируя код ещё раз, видно, что функция decimalToHex теперь занимает всего 0,04% от общего времени, перенося себя далеко вниз по списку.

Профилирование JavaScript с Chrome Developer Tools

Мы также видим, что теперь самая ресурсозатратная функция – это e.extend.merge из jQuery. Я не уверен, что эта функция делает, так как код минифицирован. Я мог бы добавить версию jQuery для разработчиков, но я вижу, что эта функция вызывается из makeColorSorter, поэтому давайте сделаем её быстрее.

Уменьшить изменение контента

Цвета радуги в сортировщике цветов генерируются на основе синусоиды. Код смотрит в центральную точку спектрума цветов и создаёт синусоиду через эту центральную точку указанной ширины. Это преобразовывает цвета по образу радуги. Мы также можем изменять цвета в радуге, изменяя частоту красного, зелёного и синего.

function makeColorSorter(frequency1, frequency2, frequency3, phase1, phase2, phase3, center, width, len) {
	for (var i = 0; i < len; ++i) {
		var red = Math.floor(Math.sin(frequency1 * i + phase1) * width + center);
		var green = Math.floor(Math.sin(frequency2 * i + phase2) * width + center);
		var blue = Math.floor(Math.sin(frequency3 * i + phase3) * width + center);
		
		console.log('red: ' + decimalToHex(red));
		console.log('green: ' + decimalToHex(green));
		console.log('blue: ' + decimalToHex(blue));
 
		var div = $('<div class="colorBlock"></div>');
		div.css('background-color', '#' + decimalToHex(red) + decimalToHex(green) + decimalToHex(blue));
		$('#colors').append(div);
    }
}

Можно убрать ещё вызовы console.log. Эти вызовы особенно вредны, потому что они каждый раз вызывают функцию decimalToHex, что значит, что decimalToHex вызывается в 2 раза чаще, чем нужно.
Эта функция сильно меняет DOM. Каждый раз при исполнении тела цикла, она добавляет новый div. Может быть, это как-то связано с тем, что вызовами e.extend.merge. Профайлер даёт возможность это проверить простым экспериментом.

Вместо добавления нового div каждый раз при прохождении цикла, я добавлю все div-теги за раз. Давайте создадим переменную для их хранения и потом добавим за 1 раз в конце.

function makeColorSorter(frequency1, frequency2, frequency3, phase1, phase2, phase3, center, width, len) {
	var colors = "";
	for (var i = 0; i < len; ++i) {
		var red = Math.floor(Math.sin(frequency1 * i + phase1) * width + center);
		var green = Math.floor(Math.sin(frequency2 * i + phase2) * width + center);
		var blue = Math.floor(Math.sin(frequency3 * i + phase3) * width + center);

		colors += '<div class="colorBlock" style="background-color: #' +
			decimalToHex(red) + decimalToHex(green) + decimalToHex(blue) + '"></div>';
	}

	$('#colors').append(colors);
}

Это небольшое изменение кода значит, что DOM будет меняться 1 раз. Тестируя это в timeline, мы видим, что время исполнения между кликом и событием Paint теперь 31 миллисекунда. Это одно изменение уменьшило время исполнения версии 4 на 87%. Мы также можем запустить профайлер и увидим, что функция e.extend.merge теперь занимает так мало времени, что даже не показывается вверху списка.

Мы можем сделать код ещё немного быстрее, полностью убрав функцию decimalToHex. CSS поддерживает RGB-цвета, поэтому нам не надо конвертировать их в 16-ричные значения. Теперь функция makeColorSorter выглядит так:

function makeColorSorter(frequency1, frequency2, frequency3, phase1, phase2, phase3, center, width, len) {
	var colors = "";
	for (var i = 0; i < len; ++i) {
		var red = Math.floor(Math.sin(frequency1 * i + phase1) * width + center);
		var green = Math.floor(Math.sin(frequency2 * i + phase2) * width + center);
		var blue = Math.floor(Math.sin(frequency3 * i + phase3) * width + center);

		colors += '<div class="colorBlock" style="background-color: rgb(' +
			red + ',' + green + ',' + blue + ')"></div>';
	}

	$('#colors').append(colors);
}

Версия 5 выполняется всего за 26 миллисекунд (ускорение в финальной версии более 92%) и использует 18 строк кода вместо 28.

Профилирование JavaScript в вашем приложении

Настоящие приложения намного сложнее, чем этот сортировщик цветов, но их профилирование состоит из тех же шагов:

  1. Определите точку отсчёта, чтобы вы знали, откуда стартуете.
  2. Изолируйте проблему, отделив её от другого кода в приложении.
  3. Сделайте его быстрее в контролируемом окружении с частым использованием timeline и профилирования.

Есть ещё некоторые правила при ускорении кода:

  • Начинайте с самых медленных частей, для того чтобы получить максимальный эффект от затраченного времени на улучшение
  • Контролируйте окружение. Если вы меняете компьютер, обновляете браузер или плагины, всегда считайте заново точку отсчёта.
  • Повторяйте анализ, чтобы предотвратить влияние аномалий на результат.

Автор: how

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js