Оптимизация CSS с помощью инструментов разработчика Chrome

Оптимизация CSS с помощью инструментов разработчика Chrome

От автора: история о том, как я выявил и решил проблему с производительностью в веб-приложении React. Я отлаживал много медленных программ, но никогда в вебе. Это была отличная возможность поэкспериментировать с инструментами и повысить свои навыки. Как выяснилось, моя проблема тяжело поддавалась анализу, но благодаря инструментам производительности Chrome, ручного профилирования и тщательного научного подхода я смог найти решение, и оптимизация CSS была произведена.

Эта статья не о React

Недавно я баловался с React. Интересный фреймворк с большими возможностями для легкой разработки, обслуживания и производительности. Мне очень хотелось его попробовать. Он сильно отличается от Backbone, front-end фреймворка, с которым я очень хорошо знаком.

Мой последний проект – это клон NMBR-9, замечательной настольной игры, которая мне нравилась. Игра похожа на тетрис, только здесь фигуры складываются вертикально, а очки получаются за самую высокую фигуру. Правила довольно легкие, а игра проходит на квадратной площадке, что идеально подходит для рендера с помощью HTML и CSS. Я подумал, такая игра будет хорошим заданием по React для меня.

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

Опытные разработчики, возможно, уже поняли, в чем проблема, и посмеиваются. Но я работал с системным программированием – за всю мою карьеру у меня почти никогда не было экранов. Поэтому для меня это было Interesting Engineering Challenge.

Измерение производительности

Инструменты профилирования

В бизнесе есть старая поговорка, которая отлично подходит к ПО: «если что-то нельзя измерить, это нельзя улучшить». Это значит, что мне нужно было сделать 2 вещи:

Понять, какая часть замедляет процесс. Так я получу цель для улучшения.

Определите время рендера одного кадра – т.е. переход от движения мыши к пикселям на экране. Это позволяет сравнить разные билды в абсолютных терминах, тем самым ответив на вопрос «а сделал ли я лучше?».

Я первый раз пользовался встроенным профайлером производительности Chrome, который дает красивое визуальное представление о том, когда какие функции работают. У Google есть хороший урок по инструменту.

Далее мне понадобилось создать свой инструмент. В браузере есть функция requestAnimationFrame(), которая принимает функцию, которую необходимо вызвать далее после отрисовки кадра (т.е. после отрисовки текущего кадра). Обычно requestAnimationFrame() используют для анимации, но я приспособил ее под свои цели. Если поместить вызов функции в React render(), то я могу измерить время от render() до законченного DOM.

render() { const renderStartTime = performance.now(); requestAnimationFrame(() => { const delta = performance.now() - renderStartTime; console.log(`render to RAF: ${delta} ms`); }); // ... do render things ...
}

Я могу бы использовать console.time() вместо performance.now(), но мне нравится иметь доступ к цифрам в случае, если я беру среднее значение, как здесь. Единственный способ получить прошедшее время из console.time() – это прочитать его глазами.

Эксперимент

Чтобы максимально контролировать испытания, мне также нужен был последовательный эксперимент. В противном случае, производительность может случайно как улучшиться, так и ухудшиться. То есть мне нужно было повторяемое начальное состояние и повторяемый набор входных данных для тестирования.

Чтобы получить начальное состояние, я прошел пол игры, а затем использовал JSON.stringify(), чтобы получить представление о состоянии игры. Значение я скопировал и вставил в вызов JSON.parse() в конструкторе компонента верхнего уровня.

Для эксперимента мне было необходимо предоставить «случайные», но повторяемые движения мыши, случайный путь по доске игры. К сожалению, обычный JS не позволяет генерировать случайное число. Однако для этого есть библиотека seedrandom. Вот моя функция случайных движений:

startRandomWalk() { this.renderTimes = []; this.setMouseLocation(10, 10); const interval = 100; Math.seedrandom('random but predictable sequence'); let steps = 0; const MAX_STEPS = 100; const intervalId = setInterval(() => { let row = this.state.mouse.row; let col = this.state.mouse.col; if (Math.random() * 2 > 1) { if (row === 0 || (row < this.state.boardHeight - 1 && Math.random() * 2 > 1)) { row += 1; } else { row -= 1; } } else { if (col === 0 || (col < this.state.boardWidth - 1 && Math.random() * 2 > 1)) { col += 1; } else { col -= 1; } } console.log(`Step ${steps}: moving mouse to ${row}-${col}`); this.setMouseLocation(row, col); steps += 1; if (steps > MAX_STEPS) { clearInterval(intervalId); } }, interval);
}

setMouseLocation() – это та же функция, которая вызывается дочерними компонентами, когда мышь проходит над ними. Это максимально близкий подход к полной последовательности без издевательств над событиями DOM. Мне оставалось подключить эту функцию к button.

Проводим тест

Инструмент готов, пора проводить тесты. Не найдя явных причин в дереве производительности Chrome, я потратил много времени, копаясь в функция React, но я не нашел в коде явного замедляющего цикла. Жаль.

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

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

Попытка 1: удаление инлайнового CSS

Инлайн CSS?!?! Да, мне стыдно. В React это так просто, а вы уже создали весь HTML в JS. Так почему не пойти дальше? По измерениям я получил 15-20% прирост производительности после удаления всего инлайн CSS. Я знал, что когда-то мне придется это сделать, поэтому приятно было убедиться, что инстинкты меня не подвели.

Это было написано в книге. У нас намного больше обработки, чем у них, поэтому лучше, чтобы различий было меньше.

15-20% звучит круто, но с моими проблемами мне нужно было где-то 95%. Продолжим искать.

Попытка 2: удаление вычисляемых стилей

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

s/background: radial-gradient\(white, (.*)\);/background-color: $1;/g

Посмотрите, как быстро! Ушли почти все визуальные лаги (проскрольте вверх и сравните с первой анимацией, разница огромная). Красота пропала, но зная, что градиенты сильно бьют по производительности, я что-нибудь придумаю.

Но в цифрах… ничего не изменилось. Ни на миллисекунду больше или меньше. То есть requestAnimationFrame() – это не все.

Упрощение

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

В итоге, получилось следующее: CSS Gradient Test (исходники). Загрузите и удивитесь: на моем MacBook 2015 года версия с градиентом проводит повторный рендер несколько секунд, а плоская версия (код тот-же) – практически мгновенна.

Более того, похожая история с requestAnimationFrame(), если посмотреть в консоль: это касается обеих версий, а версия с градиентом грузится намного дольше.

Чтобы понять почему, давайте глубже копнем в инструмент разработчика Chrome.

Это мой скриншот оценки версии с градиентом. Посмотрите на длинную зеленую полосу GPU. По умолчанию Chrome автоматически проводит сложные графические вычисления типа рендера градиентам на GPU. Мой MacBook уже старенький, и там рендер занимает время (позже я запустил тест на домашнем игровом ПК, результаты были намного лучше).

Чтобы понять, почему requestAnimationFrame() отрабатывает несколько секунд прежде, чем кадр появляется на экране, можно взглянуть на другую вкладку производительности – график summary. Ниже представлен график за тот же период.

Обратите внимание, что отсутствует категория GPU – всего 2.5 секунды бездействия. Chrome готов начать работу над кадром, как только предыдущий ушел в GPU. В большинстве случаев это нормально, но у нас самый худший сценарий, когда GPU работает намного тяжелее, чем все, что проходит в основном процессоре.

Стоит отметить, что хотя разница между работой CPU и GPU очевидна в этом патологическом сценарии, в моей реализации NMBR-9 разница была достаточно мала, и сначала мне удавалось ее игнорировать. Даже если бы я заметил это в начале, мне пришлось бы провести подобную последовательность тестов, чтобы найти проблему.

Выводы

Инструмент производительности Chrome дает намного лучший UX для разработчика по сравнению с другими инструментами, с которыми я работал в системной среде. Есть куда расти. Даже у лучших инструментов есть слепые зоны

Принципы настройки производительности одинаково хорошо подходят к системного программированию и к веб-программированию:

Измерение имеет ключевое значение, как относительно (что в коде медленно), так и абсолютно (изменилось ли что-то после правки)

Создайте последовательность экспериментов для запуска и управления, чтобы доверять своим результатам

Я написал этот пост после решения проблемы, из-за чего может показаться, что проблема не была такой и сложной. На самом деле, я провел много часов в исследованиях, замешательстве, чесании головы и тяжелой работы, чтобы понять причину. Уверен, что мою технику можно улучшить и дальше!

Большое спасибо моему инструктору, Charles за бесконечные идеи

Автор: Dan Roberts

Источник: http://adadevacademy.tumblr.com/

Редакция: Команда webformyself.