Стоимость JavaScript в 2019 году
Примечание: Если вам больше нравится смотреть презентацию, чем читать статьи, наслаждайтесь видео ниже! Если нет, пропустите видео и продолжайте читать.
Одним из крупнейших изменений в стоимости JavaScript за последние несколько лет стало улучшение скорости анализа и компиляции скриптов браузерами. В 2019 году основными затратами на обработку скриптов стали загрузка и время выполнения на процессоре.
Взаимодействие пользователя может быть задержано, если главный поток браузера занят выполнением JavaScript, поэтому оптимизация узких мест во времени выполнения скрипта и сети может быть эффективной.
Практические рекомендации на высоком уровне
Что это значит для веб-разработчиков? Затраты на анализ и компиляцию уже не такие медленные, как мы думали раньше. Три основные задачи для пакетов JavaScript:
- Улучшить время загрузки
- Держите ваши пакеты JavaScript маленькими, особенно для мобильных устройств. Небольшие пакеты ускоряют загрузку, уменьшают использование памяти и снижают затраты на процессор.
- Избегайте создания единого большого пакета; если пакет превышает ~50–100 кБ, разделите его на несколько небольших пакетов. (С мультиплексированием HTTP/2 несколько запросов и ответов могут быть отправлены одновременно, снижая накладные расходы на дополнительные запросы.)
- На мобильных устройствах лучше отправлять гораздо меньше, особенно из-за скорости сети, но также для снижения простого использования памяти.
- Улучшить время выполнения
- Избегайте долгих задач, которые могут удерживать главный поток и смещать момент, когда страницы становятся интерактивными. После загрузки время выполнения скрипта является теперь основной затратой.
- Избегайте больших встроенных скриптов (поскольку они все еще анализируются и компилируются на главном потоке). Хорошее правило: если скрипт больше 1 кБ, избегайте его встраивания (также потому, что 1 кБ — это момент, когда кэширование кода начинает работать для внешних скриптов).
Почему важны время загрузки и выполнения?
Почему важно оптимизировать время загрузки и выполнения? Время загрузки критично для сетей низкого уровня. Несмотря на рост 4G (и даже 5G) по всему миру, наши эффективные типы соединений остаются непостоянными, и многие из нас сталкиваются с скоростями, которые ощущаются как 3G (или хуже), когда мы находимся в пути.
Время выполнения JavaScript важно для телефонов с медленным процессором. Из-за различий в процессорах, графических процессорах и тепловом троттлинге существует огромное различие в производительности между телефонами высокого и низкого уровня. Это важно для производительности JavaScript, так как выполнение зависит от процессора.
На самом деле, из общего времени загрузки страницы в браузере, таком как Chrome, до 30% этого времени может быть потрачено на выполнение JavaScript. Ниже показан процесс загрузки страницы сайта с довольно типичной нагрузкой (Reddit.com) на высокопроизводительном настольном компьютере:
На мобильных устройствах выполнение JavaScript Reddit на среднем телефоне (Moto G4) занимает в 3–4 раза больше времени по сравнению с устройством высокого уровня (Pixel 3) и более чем в 6 раз больше времени на устройстве низкого уровня (<$100 Alcatel 1X):
Примечание: У Reddit разные версии для компьютеров и мобильных браузеров, поэтому результаты MacBook Pro нельзя сравнивать с другими результатами.
Когда вы пытаетесь оптимизировать время выполнения JavaScript, следите за долгими задачами, которые могут монополизировать поток пользовательского интерфейса на длительное время. Это может блокировать выполнение критических задач, даже если страница выглядит визуально готовой. Разделяйте их на более мелкие задачи. Разбивая код на части и приоритизируя порядок его загрузки, вы сможете сделать страницы интерактивными быстрее и снизить задержку ввода.
Что сделала V8 для улучшения разбора/компиляции?
Скорость разбора JavaScript в V8 увеличилась в 2 раза с версии Chrome 60. В то же время, стоимость разбора (и компиляции) стала менее заметной/важной благодаря другой работе по оптимизации в Chrome, которая выполняет это параллельно.
V8 сократил объем работы по разбору и компиляции на основном потоке в среднем на 40% (например, на 46% в Facebook, на 62% в Pinterest), максимальное улучшение составило 81% (YouTube), выполняя разбор и компиляцию в рабочем потоке. Это дополняет существующий разбор/компиляцию вне основного потока.
Мы также можем визуализировать влияние изменений на время работы ЦП в разных версиях V8 в выпусках Chrome. За то же время, которое требуется Chrome 61 для разбора JS Facebook, Chrome 75 теперь может разобрать JS Facebook и 6 раз JS Twitter.
Давайте углубимся в то, как удалось достичь этих изменений. Вкратце, ресурсы сценария могут быть потоково разобраны и скомпилированы в рабочем потоке, что означает:
- V8 может разбирать+компилировать JavaScript без блокировки основного потока.
- Потоковая обработка начинается, как только полный HTML-парсер встречает
<script>
тег. Для блокирующих сценариев HTML-парсер ждет, а для асинхронных сценариев продолжает. - Для большинства реальных скоростей подключения V8 разбирает быстрее, чем загрузка, поэтому V8 завершает разбор+компиляцию через несколько миллисекунд после загрузки последних байтов сценария.
Не совсем короткое объяснение... Ранние версии Chrome загружали весь сценарий перед началом его разбора, что было простым подходом, но не полностью использовало ресурсы ЦП. Между версиями 41 и 68 Chrome начал разбирать асинхронные и отложенные сценарии на отдельном потоке сразу после начала загрузки.
В Chrome 71 был осуществлен переход к задаче-ориентированной настройке, где планировщик мог одновременно разбирать несколько асинхронных/отложенных сценариев. Влияние этого изменения составило ~20% сокращение времени разбора на основном потоке, что дало общий ~2% улучшение TTI/FID, измеренное на реальных веб-сайтах.
В Chrome 72 мы переключились на использование потоковой обработки в качестве основного способа разбора: теперь также регулярно синхронные сценарии разбираются таким образом (за исключением встроенных сценариев). Мы также прекратили отмену потокового разбора задач, если основной поток его требует, поскольку это лишь дублирует уже выполненную работу.
Предыдущие версии Chrome поддерживали потоковый разбор и компиляцию, когда исходные данные сценария, поступающие из сети, должны были попасть на основной поток Chrome перед их передачей потоковому разборщику.
Это часто приводило к тому, что потоковый разборщик ожидал данные, которые уже поступили из сети, но еще не были переданы потоковому заданию, так как были заблокированы другой работой на основном потоке (например, HTML-разбором, разметкой или выполнением JavaScript).
Мы сейчас экспериментируем с началом разбора при предварительной загрузке, и необходимость bounce на основной поток была ограничением до этого.
Презентация Лешека Свирски на BlinkOn предоставляет больше деталей:
Как изменения отражаются в DevTools?
В дополнение к вышеописанному была проблема в DevTools, которая отображала всю задачу разборщика так, будто она использует ЦП (полный блок). Однако разборщик блокируется, если ему не хватает данных (которые должны пройти через основной поток). С тех пор, как мы перешли от одного потока потокового разбора к потоковым задачам, это стало очевидным. Вот что вы могли увидеть в Chrome 69:
Задача «разбор скрипта» отображается как занимающая 1,08 секунды. Однако разбор JavaScript на самом деле не настолько медленный! Большая часть этого времени тратится впустую, просто ожидая передачи данных на главный поток.
Chrome 76 показывает иную картину:
В целом панель производительности DevTools отлично подходит для получения общего представления о том, что происходит на вашей странице. Для подробных метрик, связанных с V8, таких как время разбора и компиляции JavaScript, мы рекомендуем использовать Chrome Tracing с Runtime Call Stats (RCS). В результатах RCS Parse-Background
и Compile-Background
показывают, сколько времени было затрачено на разбор и компиляцию JavaScript вне главного потока, тогда как Parse
и Compile
фиксируют метрики главного потока.
Каковы реальные последствия этих изменений?
Давайте рассмотрим примеры реальных сайтов и то, как применяется потоковая передача скриптов.
Reddit.com имеет несколько объединений размером более 100 кБ, которые завернуты во внешние функции, вызывая много ленивой компиляции на главном потоке. На диаграмме выше время главного потока имеет первостепенное значение, так как его занятость может задерживать интерактивность. Reddit тратит большую часть времени на главный поток с минимальным использованием фонового потока.
Они могли бы извлечь выгоду из разбиения своих крупных объединений на меньшие (например, по 50 кБ каждое) без обертки, чтобы максимизировать параллелизацию — это позволило бы каждому объединению разбираться и компилироваться отдельно, уменьшая разбор/компиляцию на главном потоке во время запуска.
Также можно рассмотреть сайт, например, Facebook.com. Facebook загружает ~6 MB сжатого JS через ~292 запроса, некоторые из них асинхронные, некоторые предварительно загруженные, а некоторые запрашиваются с более низким приоритетом. Многие их скрипты очень маленькие и детализированные — это может помочь с общей параллелизацией на фоновом потоке, так как эти меньшие скрипты могут одновременно разбираться и компилироваться потоковым способом.
Заметьте, вы, скорее всего, не Facebook и, вероятно, не имеете долгоживущего приложения, как у Facebook или Gmail, где такое количество скриптов может быть оправдано на настольных устройствах. Однако в целом старайтесь делать свои объединения грубыми и загружать только то, что необходимо.
Хотя большая часть работы по разбору и компиляции JavaScript может происходить потоковым способом на фоновом потоке, часть работы все равно должна выполняться на главном потоке. Когда главный поток занят, страница не может реагировать на ввод пользователя. Учитывайте влияние как загрузки, так и выполнения кода на ваш UX.
Примечание: В настоящее время не все движки JavaScript и браузеры реализуют потоковую передачу скриптов как оптимизацию загрузки. Тем не менее, мы считаем, что предоставленные рекомендации ведут к хорошему опыту пользователя в целом.
Стоимость разбора JSON
Поскольку грамматика JSON намного проще грамматики JavaScript, разбор JSON может выполняться более эффективно. Эти знания можно применять для улучшения производительности запуска веб-приложений, которые содержат большие JSON-подобные объектные литералы конфигураций (например, встроенные магазины Redux). Вместо того, чтобы включать данные как объектный литерал JavaScript, как показано ниже:
const data = { foo: 42, bar: 1337 }; // 🐌
…они могут быть представлены в виде JSON-строки и затем разобраны с помощью JSON на этапе выполнения:
const data = JSON.parse('{"foo":42,"bar":1337}'); // 🚀
Пока JSON-строка выполняется только один раз, подход с JSON.parse
значительно быстрее по сравнению с объектным литералом JavaScript, особенно при холодных загрузках. Хорошее практическое правило — применять эту технику для объектов размером 10 кБ или больше — но, как всегда, с советами по производительности, измеряйте реальное влияние, прежде чем вносить изменения.
Следующее видео детально объясняет, откуда берется разница в производительности, начиная с отметки 02:10.
JSON.parse
», представлено Маттиасом Биненсом на #ChromeDevSummit 2019.См. наше объяснение функции JSON ⊂ ECMAScript для примера реализации, которая, принимая произвольный объект, генерирует корректную программу JavaScript, которая JSON.parse
этот объект.
Существует дополнительный риск при использовании простых объектных литералов для больших объемов данных: они могут быть разобраны дважды!
- Первый проход происходит, когда литерал предварительно разбирается.
- Второй проход происходит, когда литерал разбирается лениво.
Первого прохода избежать нельзя. К счастью, второго прохода можно избежать, поместив объектный литерал на верхний уровень или внутри PIFE.
Что насчет повторной обработки при повторных визитах?
Оптимизация (байт)кэширования кода в V8 может помочь. Когда скрипт запрашивается впервые, Chrome загружает его и передает в V8 для компиляции. Он также сохраняет файл в кэше на диске браузера. Когда файл JS запрашивается во второй раз, Chrome берет файл из кэша браузера и снова передает в V8 для компиляции. На этот раз скомпилированный код сериализуется и прикрепляется к кэшированному файлу скрипта в виде метаданных.
На третий раз Chrome берет файл и его метаданные из кэша и передает их обоих в V8. V8 десериализует метаданные и может пропустить компиляцию. Кэширование кода срабатывает, если первые два визита происходят в течение 72 часов. Chrome также поддерживает активное кэширование кода, если используется служебный работник для кэширования скриптов. Подробнее о кэшировании кода можно прочитать в кэшировании кода для веб-разработчиков.
Выводы
В 2019 году время загрузки и выполнения — это основные проблемы при загрузке скриптов. Старайтесь использовать небольшой набор синхронных (встроенных) скриптов для содержимого выше фолда и один или несколько отложенных скриптов для остальной части страницы. Разбейте свои большие пакеты, чтобы сосредоточиться только на передаче кода, который нужен пользователю тогда, когда он необходим. Это максимизирует параллелизацию в V8.
На мобильных устройствах вам потребуется передавать значительно меньше скриптов из-за ограничений сети, потребления памяти и времени выполнения для медленных процессоров. Сбалансируйте задержку и кэшируемость, чтобы максимально увеличить объем работы по разбору и компиляции, которая может быть выполнена вне основного потока.