Статьи

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

В этом посте есть много вещей, которые можно охватить широким и дико меняющимся ландшафтом. Это также тема, которая охватывает всех любимых: JS Framework of Month ™.

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

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

Установка сцены

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

Количество мобильных пользователей превысило количество пользователей настольных компьютеров в ноябре 2016 года

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

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

Даже если вы тестируете на реальных мобильных устройствах, вы, вероятно, делаете это на своем фирменном шлепающем новом флагманском телефоне за 600 долларов. Дело в том, что это не то устройство, которое есть у ваших пользователей. Среднее устройство является чем-то похожим на Moto G1 — устройство с менее чем 1 ГБ ОЗУ и очень слабым процессором и графическим процессором.

Давайте посмотрим, как это складывается при разборе среднего JS-пакета .

Уч. Хотя это изображение охватывает только время анализа и компиляции JS (подробнее об этом позже), а не общую производительность, оно сильно коррелирует и может рассматриваться как показатель общей производительности JS.

По словам Брюса Лоусона, « это всемирная паутина, а не богатая западная паутина ». Итак, ваша цель для веб-производительности — это устройство, которое примерно в 25 раз медленнее, чем ваш MacBook или iPhone. Позвольте этому погрузиться немного. Но это становится хуже. Давайте посмотрим, к чему мы на самом деле стремимся.

Что такое кодекс Performant JS?

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

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

Отвечать

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

Animate

На мониторе с частотой 60 Гц мы хотим получать постоянные 60 кадров в секунду при анимации и прокрутке. В результате получается около 16 мс на кадр. Из этого бюджета в 16 мс у вас есть 8–10 мс на выполнение всей работы, а остальное приходится на внутренние компоненты браузера и другие варианты.

Холостая работа

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

нагрузка

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

На практике стремитесь к 5-кратной интерактивной отметке. Это то, что Chrome использует в своем аудите Lighthouse .

Теперь, когда мы знаем метрики, давайте посмотрим на некоторые статистические данные :

  • 53% посещений отменяются, если загрузка мобильного сайта занимает более трех секунд
  • 1 из 2 человек ожидает загрузки страницы менее чем за 2 секунды
  • 77% мобильных сайтов загружаются в сетях 3G более 10 секунд
  • 19 секунд — это среднее время загрузки мобильных сайтов в сетях 3G.

И еще немного, любезно предоставлено Адди Османи :

  • приложения стали интерактивными за 8 секунд на рабочем столе (с помощью кабеля) и 16 секунд на мобильном телефоне (Moto G4 через 3G)
  • в среднем разработчики отправили 410KB сжатых JS для своих страниц.

Чувствуете себя достаточно разочарованным? Хорошо. Давайте приступим к работе и исправим Интернет. ✊

Контекст — это все

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

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

Прежде чем погрузиться в оптимизацию кода, подумайте над тем, что вы строите. Вы создаете фреймворк или библиотеку VDOM? Ваш код должен выполнять тысячи операций в секунду? Вы делаете критичную по времени библиотеку для обработки пользовательского ввода и / или анимации? Если нет, возможно, вы захотите перенести свое время и энергию куда-то более эффективное.

Дело не в том, что написание исходного кода не имеет значения, но обычно оно мало что дает или вообще не влияет на общую схему вещей, особенно когда речь идет о микрооптимизации. Поэтому перед тем, как приступить к аргументу переполнения стека о .forEach .map против .forEach против for , сравнивая результаты с JSperf.com, убедитесь, что вы видите лес, а не только деревья. 50 кбит / с может звучать в 50 раз лучше, чем 1 кбит / с на бумаге, но в большинстве случаев это не будет иметь значения.

Разбор, компиляция и выполнение

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

Мы говорим об уровнях абстракции здесь. Процессор в вашем компьютере работает машинный код. Большая часть кода, который вы запускаете на своем компьютере, находится в скомпилированном двоичном формате. (Я сказал код, а не программы , рассматривая все приложения Electron в наши дни.) То есть, за исключением всех абстракций на уровне ОС, он изначально работает на вашем оборудовании, никакой подготовительной работы не требуется.

JavaScript не предварительно скомпилирован. Он поступает (через относительно медленную сеть) как читаемый код в вашем браузере, который, по сути, является «ОС» для вашей программы JS.

Этот код сначала должен быть проанализирован, то есть прочитан и превращен в индексируемую компьютером структуру, которую можно использовать для компиляции. Затем он компилируется в байт-код и, наконец, машинный код, прежде чем он может быть выполнен вашим устройством / браузером.

Еще одна очень важная вещь, которую стоит упомянуть, это то, что JavaScript является однопоточным и работает в основном потоке браузера. Это означает, что одновременно может выполняться только один процесс. Если ваша временная шкала производительности DevTools заполнена желтыми пиками, когда ваш процессор загружен на 100%, у вас будут длинные / пропущенные кадры, резкая прокрутка и другие неприятные вещи.

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

Есть две вещи, которые вы должны убрать из этого раздела:

  1. Хотя это не обязательно линейно, время разбора JS масштабируется с размером пакета. Чем меньше JS вы отправляете, тем лучше.
  2. Каждый используемый вами JS-фреймворк (React, Vue, Angular, Preact …) — это еще один уровень абстракции (если только он не скомпилирован, как Svelte ). Это не только увеличит размер пакета, но и замедлит ваш код, поскольку вы не общаетесь напрямую с браузером.

Есть способы смягчить это, например, использование сервисных работников для выполнения заданий в фоновом режиме и в другом потоке, использование asm.js для написания кода, который легче компилировать в машинные инструкции, но это целая «другая тема».

Однако вы можете избегать использования каркасов анимации JS для всего и читать о том, что вызывает рисование и макеты . Используйте библиотеки только тогда, когда нет абсолютно никакой возможности реализовать анимацию, используя обычные CSS-переходы и анимации.

Даже если они используют CSS-переходы, составные свойства и requestAnimationFrame() , они все еще работают в JS в основном потоке. Они в основном просто забивают вашу модель DOM встроенными стилями каждые 16 мс, так как они мало что могут сделать. Вы должны убедиться, что все ваши JS будут выполняться менее чем за 8 мс на кадр, чтобы анимации были плавными.

CSS-анимация и переходы, с другой стороны, выполняются вне основного потока — на графическом процессоре, если реализованы качественно, не вызывая ретрансляции / перекомпоновки.

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

Web Animations API — это новый набор функций, который позволит вам создавать производительные JS-анимации из основного потока, но пока придерживайтесь CSS-переходов и методов, таких как FLIP .

Размеры комплекта — все

Сегодня это все о связках. Прошли времена Бауэра и десятки тегов <script> перед закрывающим </body> .

Теперь все дело в npm install любой новой блестящей игрушке, которую вы найдете в NPM, объединяющей их вместе с Webpack в один большой JS-файл объемом 1 МБ и заставляющем браузер ваших пользователей сканировать свои данные.

Попробуйте отправить меньше JS. Вам может не понадобиться вся библиотека Lodash для вашего проекта. Вам абсолютно необходимо использовать JS Framework? Если да, рассматривали ли вы возможность использования чего-либо, кроме React, например, Preact или HyperHTML , которые меньше чем 1/20 размера React? Вам нужен TweenMax для анимации прокрутки вверх? Удобство npm и изолированных компонентов в фреймворках имеет недостаток: первым ответом разработчиков на проблему стало использование JS. Когда у тебя есть только молоток, все выглядит как гвоздь.

Когда вы закончите обрезку сорняков и отгрузите меньше JS, попробуйте отправлять это умнее . Отправляйте то, что вам нужно, когда вам это нужно.

Webpack 3 имеет удивительные функции, называемые разделением кода и динамическим импортом . Вместо объединения всех ваших модулей JS в монолитный пакет app.js он может автоматически разбивать код с помощью синтаксиса import() и загружать его асинхронно.

Вам также не нужно использовать каркасы, компоненты и маршрутизацию на стороне клиента, чтобы получить выгоду от этого. Допустим, у вас есть сложный кусок кода, который .mega-widget ваш .mega-widget , который может быть на любом количестве страниц. Вы можете просто написать следующее в своем основном файле JS:

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

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

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

 new webpack.optimize.CommonsChunkPlugin({ name: '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, который имеет не только огромный размер файла, но также огромные затраты на синтаксический анализ и выполнение для поддержки старых браузеров.

Однако нет смысла наказывать людей, использующих современные браузеры. Подход, который я использую, и который Филипп Уолтон рассмотрел в этой статье , заключается в создании двух отдельных пакетов и их условной загрузке. Babel делает это легко с помощью babel-preset-env . Например, у вас есть один пакет для поддержки IE 11, а другой без полизаполнения для последних версий современных браузеров.

Грязный, но эффективный способ — поместить следующее во встроенный скрипт:

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

И с этим сказал, я хотел бы закончить со следующим твитом: