Советы по оптимизации производительности JavaScript: обзор

Советы по оптимизации производительности JavaScript: обзор

От автора: сегодня мы будем говорить о том, как делается в JavaScript оптимизация производительности. В этой статье многое нужно обсудить, так как тема обширная. Это также всеми любимая тема – JS фреймворк месяца.

Будем придерживаться мантры «инструменты, а не правила» и сведем к минимуму JS термины. Мы не сможем покрыть все, что касается JS производительности, в статье в 2000 слов, поэтому читайте также статьи по ссылкам и изучайте тематику самостоятельно.

Но прежде чем мы погрузимся в детали, давайте глубже поймем проблему, ответив на следующий вопрос: что считается производительным JS кодом, и как он вписывается в более широкий диапазон веб-производительности?

Настройка окружения

Во-первых, сделаем следующее: если вы тестируете исключительно на десктопе, вы отметаете более 50% пользователей.

Советы по оптимизации производительности JavaScript: обзор

Современные тенденции и подходы в веб-разработке

Узнайте алгоритм быстрого профессионального роста с нуля в сайтостроении

Узнать подробнее

Тенденция будет только расти, так как Android устройства менее 100$ являются предпочтительным каналом развивающихся рынков в сети. Эра, когда главным устройство доступа в интернет был десктоп, закончилась. Следующий миллиард пользователей, которые посетят ваш сайт, будет с мобильных устройств.

Тестирование в Chrome DevTools в режиме определенного устройства не заменяет тестирования на реальном устройстве. CPU замедление и замедление скорости сети помогает, но это совсем другое. Тестируйте на реальных устройствах.

И если вы тестируете на реальных мобильных устройствах, вы, скорее всего, делаете это на брендовом флагманском смартфоне за 600$. У ваших пользователей не такое устройство. У ваших пользователей что-то среднее — Moto G1 – устройство с менее 1Гб ОЗУ и слабыми CPU и GPU.

Посмотрим зависимость при парсинге среднего пакета JS.

Советы по оптимизации производительности JavaScript: обзор

Ого. Этот скриншот покрывает только время парсинга и компиляции JS (подробнее чуть позже), а не общую производительность, однако корреляция сильная и может рассматриваться как показатель общей производительности JS.

Процитирую Bruce Lawson – «Это всемирная паутина, а не богатая западная сеть». Поэтому ваше целевое устройство примерно в 25 раз медленнее вашего MacBook и iPhone. Разберем тему немного подробнее. Все становится хуже. Давайте посмотрим на нашу реальную цель.

Что такое производительный JS код?

Мы узнали нашу целевую платформу. Теперь мы можем ответить на следующий вопрос – что такое производительный JS код?

Не существует точного определения производительного кода, однако у нас есть пользовательская модель производительности, которую мы можем использовать – модель RAIL.

Советы по оптимизации производительности JavaScript: обзор

Ответ

Если ваше приложение отвечает на пользовательское взаимодействие менее чем за 100мс, пользователь воспринимает его как мгновенное. Это касается элементов, на которые можно нажимать, и не относится к прокрутке и drag and drop.

Анимация

На мониторе 60Гц нам необходимо получить постоянные 60 кадров в секунду для анимации и прокрутки. Это около 16мс за кадр. Из этих 16мс у вас реально есть 8-10мс. Остальное занято внутренними операциями браузера и другими отклонениями.

Работа в режиме ожидания

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

Загрузка

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

Советы по оптимизации производительности JavaScript: обзор

На практике, старайтесь уложиться с взаимодействием в 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%, то вы получите долгую/дерганую анимацию, дерганую прокрутку и т.д.

Советы по оптимизации производительности JavaScript: обзор

Прежде чем JS код начнет работать, необходимо выполнить много работы. Парсинг и компиляция занимают до 50% всего времени выполнения JS в движке Chrome V8.

Советы по оптимизации производительности JavaScript: обзор

Из этого раздела нужно запомнить:

Время парсинга 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 файле:

if (document.querySelector('.mega-widget')) {
 import('./mega-widget');
}

Webpack также требует своего времени выполнения для работы, которое он вставляет во все сгенерированные файлы .js. Если вы используете плагин commonChunks, с помощью кода ниже вы можете вытащить runtime в отдельный код:

new webpack.optimize.CommonsChunkPlugin({
  name: 'runtime',
}),

Этот код будет выталкивать runtime из всех других кусков кода в свой файл (у нас это runtime.js). Не забудьте загрузить его перед главным JS пакетом. Например:

<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 и другой без полифилов для последних версий современных браузеров.

Грязный, но эффективный способ – вставить код ниже в инлайн скрипт:

(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ć

Источник: https://www.sitepoint.com/

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

Современные тенденции и подходы в веб-разработке

Узнайте алгоритм быстрого профессионального роста с нуля в сайтостроении

Узнать подробнее

JavaScript&jQuery с нуля до профи

Пройдите пошаговый видеокурс по JavaScript&jQuery

Научиться

Метки:

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

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

Комментарии Facebook:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree