Оптимизация 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.

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

Эксперимент

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Но в цифрах… ничего не изменилось. Ни на миллисекунду больше или меньше. То есть 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

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

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

Метки:

Похожие статьи:

Комментарии Вконтакте: