От автора: как происходит в JavaScript оптимизация производительности? В этой статье нужно много всего рассказать по такой широкой и резко меняющейся области. Эта тема также затрагивает всеми любимое: JS фреймворк месяца.
Постараемся придерживаться мантры «инструменты, а не правила» и сократить умные словечки по JS до минимума. Поскольку мы не сможем уместить все по теме производительности JS в 2000 слов, прочитайте ссылки и проведите свое исследование.
Прежде чем погрузиться в детали, давайте получим широкое понимание проблемы, ответив на следующий вопрос. Что считается быстрым JS, как он вписывается в более широкий диапазон показателей веб-производительности?
Подготовка
Во-первых, сделаем следующее: если вы тестируете только на десктоп устройстве, вы исключаете более 50% пользователей.
Тенденция будет только расти, поскольку развивающийся рынок предпочитает использовать устройства Android по цене менее $100 для выхода в сеть. Эра выхода в интернет с десктопа как с основного устройства подошла к концу, и следующий миллиард интернет пользователей посетят ваши сайты в первую очередь с мобильного устройства.
Тестирование в режиме устройства в Chrome DevTools не заменяет тестов на реальном устройстве. Замедление CPU и скорости сети помогает, но это совсем другое. Тестируйте на реальных устройствах.
Даже если вы тестируете на реальном мобильном устройстве, вы можете делать это на своем новейшем флагманском смартфоне. Суть в том, что ваши пользователи сидят на других устройствах. Среднее устройство – это что-то типа Moto G1 – устройство с 1Гб ОЗУ и очень слабым CPU и GPU.
Давайте посмотрим на график парсинга среднего JS пакета.
Ого. Изображение показывает только парсинг и время компиляции JS (подробнее об этом позже), а не общую производительность. Но данные все равно можно сопоставить с общей производительность JS.
Процитирую Bruce Lawson: «это всемирная сеть, а не богатая западная сеть». Поэтому ваше целевое устройство примерно в 25 раз медленнее вашего MacBook или iPhone. Подумайте об этом. Но все становится еще хуже. Давайте посмотрим, на что мы на самом деле нацеливаемся.
Что именно представляет собой быстрый JS-код?
Мы выяснили нашу целевую платформу. Теперь давайте ответим на вопрос: что такое быстрый JS-код?
Нет абсолютной классификации определения быстрого кода, но у нас точно есть модель производительности, ориентированная на пользователя, которую можно использовать: модель RAIL.
Ответ
Если приложение отвечает на действие пользователя менее чем за 100мс, пользователь воспринимает ответ мгновенно. Это касается нажимаемых элементов, но не относится к прокрутки и перетаскиванию.
Анимация
На мониторах 60Hz постоянно необходимо иметь 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 библиотеку? Должен ли ваш код делать тысячи операций в секунду? Вы делаете библиотеку с упором на время для обработки вводимых пользователем данных и/или анимации? Если нет, можете потратить время и энергию на что-то более влиятельное.
Не то чтобы писать быстрый код бесполезно, но обычно это мало влияет на общую схему вещей, особенно когда речь идет о микрооптимизации. Поэтому прежде чем перейти в споры на Stack Overflow по циклам .map или .forEach или for, сравнивая результаты с JSperf.com, убедитесь что видите целую картину. На бумаге 50k ops/s может смотреться в 50 раз лучше, чем 1k ops/s, но в большинстве случаев разницы нет.
Парсинг, компиляция и выполнение
Фундаментальная проблема большинства медленного JS кроется не в запуске кода, а во всех тех шагах, которые необходимо предпринять перед выполнением кода.
Мы говорим об уровнях абстракции. CPU на вашем компьютере запускает машинный код. Большая часть кода, запускаемого на компьютере, находится в скомпилированном двоичном формате. (учитывая все Electron приложения сегодня, я сказал код, а не программы) То есть код запускается непосредственно на железе, все уровни абстракции ОС отметаются. Подготовка не нужна.
JS не компилируется предварительно. Он попадает в ваш браузер (по медленным сетям) как читаемый код, что, в сущности, является ОС для JS программ.
Сперва, этот код необходимо распарсить – т.е. прочитать и перевести в индексируемую компьютером структуру, которую можно использовать для компиляции. Далее он компилируется в код байтов и далее в машинный код, после чего уже может выполняться на устройстве/браузере.
Также очень важно отметить, что JS работает в одном потоке и запускается на главном потоке браузера. То есть за раз можно запустить только один процесс. Если таймлайн в DevTools заполнен желтыми всплесками, нагружая CPU на 100%, вы получите длинные/отброшенные кадры, трясущуюся прокрутку и другие неприятности.
Все это нужно выполнить перед тем, как ваш JS начнет работать. Парсинг и компиляция занимают до 50% общего времени выполнения JS на движке V8 Chrome.
Из этого раздела нужно запомнить:
Необязательно линейно, но время парсинга JS увеличивается с ростом размера пакета. Чем меньше скачивать JS, тем лучше.
Каждый используемый JS фреймворк (React, Vue, Angular, Preact…) – это еще один уровень абстракции (если он не скомпилирован предварительно, как Svelte). Это не только увеличивает размер пакета, но и замедляет код, потому что вы не говорите с браузером напрямую.
Есть способы смягчить это, например, использовать сервис воркеры для выполнения заданий в фоновом режиме и в другом потоке, используя asm.js для написания кода, который легче скомпилируется для машинных инструкций. Это целая отдельная тема.
Однако вы можете не использовать фреймворки анимации JS для всего и прочитать, что вызывает орисовку и макетирование. Используйте библиотеки только тогда, когда абсолютно невозможно реализовать анимацию, используя обычные переходы и анимации CSS.
Несмотря на то, что они могут использовать переходы CSS, составные свойства и requestAnimationFrame (), они все еще работают в JS, в основном потоке. Они в основном просто забивают ваш DOM встроенными стилями каждые 16 мс, поскольку делать им больше нечего. Вы должны убедиться, что весь JS будет выполняться менее 8 мс на кадр, чтобы анимация была плавной.
С другой стороны, анимации и переходы CSS сбиваются с основного потока — на графическом процессоре, если они реализованы, не вызывая relayouts/reflows.
Учитывая, что большинство анимаций работают либо во время загрузки, либо при взаимодействии с пользователем, это может дать вашим веб-приложениям столь необходимое пространство для маневра.
Web Animations API — это набор функций, который позволит вам выполнять анимацию JS с основного потока, но на данный момент придерживаться переходов CSS и таких методов, как FLIP.
Размер пакета – все
Сегодня все в пакетах. Прошли те времена Bower и десятков тегов script перед закрывающим тегом body.
Теперь все делается через npm install, устанавливая любую новую игрушку, которую вы найдете на NPM, объединив их вместе с Webpack в огромном одном JS-файле размером 1 МБ и забивая браузер своих пользователей в обход, закрывая тарифы.
Попробуйте отправлять меньше JS. Вам может не понадобиться вся библиотека Lodash для вашего проекта. Вам точно нужно использовать JS фреймворк? Если да, смотрели ли вы что-то отличное от React, например Preact или HyperHTML, размер которых меньше 1/20 размера React? Вам нужен 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 требует свой собственный runtime для работы, и он внедряется во все созданные файлы .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, который имеет не только огромный размер файла, но и огромный синтаксический анализ и стоимость выполнения, чтобы поддерживать старые браузеры.
Нет смысла наказывать людей, которые используют современные браузеры. Подход, который я использую, и который Филипп Уолтон рассмотрел в этой статье, состоит в том, чтобы создать два отдельных пакета и загрузить их условно. Babel делает это легко с babel-preset-env. Например, у вас есть один пакет для поддержки IE 11, а другой без полифилов для последних версий современных браузеров.
Грязный, но эффективный способ заключается в следующем:
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.