От автора: сегодня мы будем говорить о том, как делается в JavaScript оптимизация производительности. В этой статье многое нужно обсудить, так как тема обширная. Это также всеми любимая тема – JS фреймворк месяца.
Будем придерживаться мантры «инструменты, а не правила» и сведем к минимуму JS термины. Мы не сможем покрыть все, что касается JS производительности, в статье в 2000 слов, поэтому читайте также статьи по ссылкам и изучайте тематику самостоятельно.
Но прежде чем мы погрузимся в детали, давайте глубже поймем проблему, ответив на следующий вопрос: что считается производительным JS кодом, и как он вписывается в более широкий диапазон веб-производительности?
Настройка окружения
Во-первых, сделаем следующее: если вы тестируете исключительно на десктопе, вы отметаете более 50% пользователей.
Тенденция будет только расти, так как Android устройства менее 100$ являются предпочтительным каналом развивающихся рынков в сети. Эра, когда главным устройство доступа в интернет был десктоп, закончилась. Следующий миллиард пользователей, которые посетят ваш сайт, будет с мобильных устройств.
Тестирование в Chrome DevTools в режиме определенного устройства не заменяет тестирования на реальном устройстве. CPU замедление и замедление скорости сети помогает, но это совсем другое. Тестируйте на реальных устройствах.
И если вы тестируете на реальных мобильных устройствах, вы, скорее всего, делаете это на брендовом флагманском смартфоне за 600$. У ваших пользователей не такое устройство. У ваших пользователей что-то среднее — Moto G1 – устройство с менее 1Гб ОЗУ и слабыми CPU и GPU.
Посмотрим зависимость при парсинге среднего пакета JS.
Ого. Этот скриншот покрывает только время парсинга и компиляции JS (подробнее чуть позже), а не общую производительность, однако корреляция сильная и может рассматриваться как показатель общей производительности JS.
Процитирую Bruce Lawson – «Это всемирная паутина, а не богатая западная сеть». Поэтому ваше целевое устройство примерно в 25 раз медленнее вашего MacBook и iPhone. Разберем тему немного подробнее. Все становится хуже. Давайте посмотрим на нашу реальную цель.
Что такое производительный JS код?
Мы узнали нашу целевую платформу. Теперь мы можем ответить на следующий вопрос – что такое производительный JS код?
Не существует точного определения производительного кода, однако у нас есть пользовательская модель производительности, которую мы можем использовать – модель RAIL.
Ответ
Если ваше приложение отвечает на пользовательское взаимодействие менее чем за 100мс, пользователь воспринимает его как мгновенное. Это касается элементов, на которые можно нажимать, и не относится к прокрутке и drag and drop.
Анимация
На мониторе 60Гц нам необходимо получить постоянные 60 кадров в секунду для анимации и прокрутки. Это около 16мс за кадр. Из этих 16мс у вас реально есть 8-10мс. Остальное занято внутренними операциями браузера и другими отклонениями.
Работа в режиме ожидания
Если у вас есть тяжелые и постоянно работающие задачи, постарайтесь разбить их на небольшие кусочки, чтобы главная ветка могла реагировать на пользовательское взаимодействие. У вас не должно быть задач, которые тормозят пользователя более чем на 50мс.
Загрузка
Ваша задача – загрузка страниц менее чем за 1000мс. Все что длится больше этого времени, раздражает пользователей. На мобильных устройствах довольно сложно добиться таких показателей, так как страница должна взаимодействовать с пользователем, а не просто рендериться и прокручиваться. На практике времени еще меньше:
На практике, старайтесь уложиться с взаимодействием в 5 секунд. Именно такое время задано в Chrome Lighthouse audit.
Мы знаем цифры, теперь посмотрим на статистику:
53% посетителей покидают сайт, если мобильная версия загружается более 3 секунд
1 из 2 людей ожидает, что страница загрузится менее чем за 2 секунды
77% мобильных сайтов загружаются более 10 секунд на 3G соединении
19 секунд – среднее время загрузки мобильного сайта на 3G соединении
И еще немного от Addy Osmani:
Приложения становятся интерактивными за 8 секунд на десктопе (по проводу) и за 16 на мобильных устройствах (Moto G4 на 3G+)
В среднем разработчики отгружают 410Кб сжатого JS на страницы
Разочарованы? Хорошо. Примемся за работу и исправим положение.
Контекст – все
Вы могли заметить, что самый большой недостаток – время загрузки сайта. В частности, загрузка JS, парсинг, компиляция и выполнение. Как-то улучшить это нельзя, но можно загружать меньше JS и делать это умнее.
Но как начет фактической работы, которую выполняет ваш код помимо простой загрузки сайта? Здесь есть где улучшить производительность, так ведь?
Прежде чем погрузиться в оптимизацию кода, подумайте, что вы создаете. Вы создаете фреймворк или VDOM библиотеку? Ваш код должен выполнять тысячи операций в секунду? Вы пишите библиотеку с упором на время обработки пользовательского ввода и/или анимации? Если нет, то вы можете потратить время и энергию на что-то более значимое.
Суть не в том, что писать производительный код бесполезно. Просто обычно это мало влияет на общую схему вещей, особенно когда речь идет о микрооптимизации. Поэтому прежде чем вы начнете спорить о .map vs .forEach vs for на Stack Overflow, сравнивая результаты с JSperf.com, убедитесь, что вы видите целый лес, а не деревья. 50Кб ops/s звучит в 50 раз лучше чем 1Кб ops/s, но на деле разницы никакой.
Парсинг, компиляция и выполнение
Основная проблема медленного JS кода заключается не в выполнении самого кода, а во всех шагах, которые необходимы выполнить перед выполнением самого кода.
Мы говорим об уровнях абстракции. CPU в вашем компьютере запускает машинные коды. Большая часть запускаемых на вашем ПК кодов скомпилированы в бинарный формат (я сказал именно код, а не программы из-за Electron приложений). Если не учитывать все абстракции ОС, код запускается прямо на железе без предварительных подготовок.
JS не компилируется заранее. Он поступает (по довольно медленному соединению) в форме читаемого кода в браузер, который является ОС для вашего JS кода.
Этот код первым делом парсится – т.е. читается и переводится в структуру, индексируемую компьютером, которую можно использовать для компиляции. Далее код компилируется в байткод и на далее в машинный код, прежде чем он может быть выполнен в браузере/на устройстве.
Также стоит сказать, что JS однопотоковый язык и запускается на главной ветке браузера. То есть за один промежуток времени можно запустить только один процесс. Если таймлайн производительности в DevTools насыщен желтыми пиками, занимающими CPU на 100%, то вы получите долгую/дерганую анимацию, дерганую прокрутку и т.д.
Прежде чем JS код начнет работать, необходимо выполнить много работы. Парсинг и компиляция занимают до 50% всего времени выполнения JS в движке Chrome V8.
Из этого раздела нужно запомнить:
Время парсинга JS увеличивается по мере роста размера пакета, хотя и не линейно. Чем меньше JS, тем лучше.
Каждый используемый JS фреймворк (React, Vue, Angular, Preact…) – еще один уровень абстракции (если он не заранее скомпилирован, как Svelte). Это не только увеличит размер пакета, но и замедлит код, так как не общаетесь с браузером напрямую.
Вы можете не использовать JS фреймворки для анимации всего что не попадя, а также можете прочитать, что вызывает перерисовки и макетирование. Используйте библиотеки только тогда, когда совсем нет способа реализовать анимацию через обычные CSS переходы и CSS анимацию.
Несмотря на использование CSS переходов, составных свойств и requestAnimationFrame(), все это запускается в JS на главном потоке. Все это, в основном, забивает ваш DOM инлайновыми стилями каждый 16мс, так как по-другому они не умеют. Чтобы анимация была плавной, весь JS должен выполняться менее 8мс за кадр.
CSS анимация и переходы запускаются не в главном потоке, а нА GPU при правильной реализации без макетирования и рефлоу.
Учитывая что почти вся анимация запускается либо во время загрузки, либо во время пользовательского взаимодействия, это даст вашему приложению такое нужное место для маневра.
Web Animations API – предлагаемый набор функций, с помощью которого можно делать производительную JS анимацию с главного потока. Но пока что сосредоточимся на CSS переходах и техниках типа FLIP.
Размер пакета – все
Сегодня все завязано на пакеты. Прошли времена Bower и нескончаемых тегов script перед закрывающим body.
Сейчас все что вы найдете в NPM, можно установить через npm install, добавляя все в один пакет через Webpack в один огромный JS файл 1Мб, забивая браузер пользователей и их тарифный план.
Постарайтесь отгружать меньше JS. Возможно, вам не нужна вся библиотека Lodash для проекта. Вам действительно нужно использовать JS фреймворк? Если да, учли ли вы что-нибудь помимо React. Например, Preact или HyperHTML, которые меньше React более чем в 20 раз. Нужна ли TweenMax для всех анимаций прокрутки до шапки страницы? У удобства npm и изолированных компонентов в фреймворках есть недостатки – первый ответ разработчиков на проблему привел к увеличению JS кода. Если у вас есть только молоток, все похоже на гвозди.
Когда почистите свой JS, попробуйте отгружать его более умным способом. Отгружайте что нужно и когда нужно.
В Webpack 3 есть замечательные функции – разбиение кода и динамические импорты. Не загоняйте все JS модули в один app.js. Код автоматически можно разбить с помощью import() и загружать асинхронно.
Не нужно использовать фреймворки, компоненты и клиентский роутинг. Скажем, у вас есть сложный кусок кода, отвечающий за .mega-widget, который может быть на любом количестве страниц. Можно просто написать следующее в главном JS файле:
1 2 3 |
if (document.querySelector('.mega-widget')) { import('./mega-widget'); } |
Webpack также требует своего времени выполнения для работы, которое он вставляет во все сгенерированные файлы .js. Если вы используете плагин commonChunks, с помощью кода ниже вы можете вытащить runtime в отдельный код:
1 2 3 |
new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', }), |
Этот код будет выталкивать runtime из всех других кусков кода в свой файл (у нас это runtime.js). Не забудьте загрузить его перед главным JS пакетом. Например:
1 2 |
<script src="runtime.js"> <script src="main-bundle.js"> |
Также можно рассказать про транспиллинг кода и полифилы. Если вы пишите современный код (ES6+) JavaScript, вы, скорее всего, используете Babel для транспиллинга кода в ES5. Транспиллинг увеличивает размер файла не только из-за большего количества символов, но и из-за сложности. Часто в отличие от ES6+ кода, здесь есть регрессии производительности.
Учитывая все это, вы, скорее всего, используете пакет babel-polyfill и whatwg-fetch для патчинга недостающих функций в старых браузерах. Если вы пишите код через async/await, вы также транспиллируете его с помощью генераторов, необходимых для подключения regenerator-runtime…
Суть в том, что вы добавляете почти 100Кб к JS пакету, что не только огромный размер, но и сильно сказывается на парсинге и выполнении для поддеркжи старых браузеров.
Но не стоит карать людей, использующих современные браузеры. Я использую подход, который Philip Walton разобрал в своей статье – создание двух отдельных пакетов и загрузка по условию. В Babel это можно легко сделать через babel-preset-env. Например, у вас один пакет для поддержки IE11 и другой без полифилов для последних версий современных браузеров.
Грязный, но эффективный способ – вставить код ниже в инлайн скрипт:
1 2 3 4 5 6 7 8 9 |
(function() { try { new Function('async () => {}')(); } catch (error) { // create script tag pointing to legacy-bundle.js; return; } // create script tag pointing to modern-bundle.js;; })(); |
Если браузер не знает функцию async, мы думаем, что это старый браузер и подсовываем пакет с полифилами. В противном случае пользователь получает современный вариант.
Заключение
Мы бы хотели, чтобы вы из этой статьи выяснили, что JS – дорогое удовольствие, и его нужно использовать аккуратно.
Протестируйте производительность сайта на слабых устройствах с реальной скоростью сети. Ваш сайт должен загружаться быстро, а также максимально взаимодействовать с пользователем. То есть нужно отгружать меньше JS, отгружать его быстрее с помощью любых инструментов. Код всегда должен быть минифицирован, разделен на маленькие, управляемые пакеты, которые загружаются асинхронно при любой возможности. Также проверьте активность HTTP/2 для более быстрой параллельной передачи и gzip/Brotli сжатие для снижения передаваемых JS файлов.
Автор: Ivan Čurić
Источник: //www.sitepoint.com/
Редакция: Команда webformyself.