Хабрахабр

[Перевод] Цена JavaScript в 2019 году

За последние несколько лет в том, что называют «ценой JavaScript», наблюдаются серьёзные положительные изменения благодаря повышению скорости парсинга и компиляции скриптов браузерами. Сейчас, в 2019 году, главными составляющими нагрузки на системы, создаваемой JavaScript, являются время загрузки скриптов и время их выполнения.

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

Общие практические рекомендации по оптимизации сайтов

Что вышесказанное означает для веб-разработчиков? Дело тут в том, что затраты ресурсов на парсинг (разбор, синтаксический анализ) и компиляцию скриптов уже не так серьёзны, как раньше. Поэтому при анализе и оптимизации JavaScript-бандлов разработчикам стоит прислушаться к следующим трём рекомендациям:

  1. Стремитесь снизить время, необходимое на загрузку скриптов.
    • Постарайтесь, чтобы ваши JS-бандлы имели бы небольшой размер. Особенно это важно для сайтов, рассчитанных на мобильные устройства. Использование маленьких бандлов улучшает время загрузки кода, снижает уровень использования памяти, уменьшает нагрузку на процессор.
    • Старайтесь, чтобы весь код проекта не был бы представлен в виде одного большого бандла. Если размер бандла превышает примерно 50-100 Кб — разделите его на отдельные фрагменты небольшого размера. Благодаря HTTP/2-мультиплексированию одновременно может выполняться отправка нескольких запросов к серверу и обработка нескольких ответов. Это снижает нагрузку на систему, связанную с необходимостью выполнения дополнительных запросов на загрузку данных.
    • Если вы работаете над мобильным проектом — постарайтесь, чтобы код был бы как можно меньшего размера. Эта рекомендация связана с невысокими скоростями передачи данных по мобильным сетям. Кроме того, стремитесь к экономному использованию памяти.
  2. Стремитесь снизить время, необходимое на выполнение скриптов.
    • Избегайте использования длительных задач, которые способны на долгое время нагружать главный поток и увеличивать время, необходимое на то, чтобы страницы оказывались бы в состоянии, в котором с ними могут взаимодействовать пользователи. В текущих условиях выполнение скриптов, происходящее после того, как они оказываются загруженными, вносит основной вклад в «цену JavaScript».
  3. Не встраивайте большие фрагменты кода в страницы.
    • Здесь стоит придерживаться следующего правила: если размер скрипта превышает 1 Кб — постарайтесь не встраивать его в код страницы. Одной из причин этой рекомендации является тот факт, что 1 Кб — это тот предел, после которого в Chrome начинает работать кэширование кода внешних скриптов. Кроме того, учитывайте то, что разбор и компиляция встроенных скриптов всё ещё выполняются в главном потоке.

Почему так важно время загрузки и выполнения скриптов?

Почему в современных условиях важно оптимизировать время загрузки и выполнения скриптов? Время загрузки скриптов чрезвычайно важно в ситуациях, когда с сайтами работают через медленные сети. Несмотря на то, что в мире всё сильнее распространяются сети 4G (и даже 5G), свойство NetworkInformation.effectiveType во многих случаях использования мобильных соединений с Интернетом демонстрирует показатели, находящиеся на уровне 3G-сетей или даже на более низких уровнях.

Из-за того, что в мобильных устройствах используются различные CPU и GPU, из-за того, что при перегреве устройств, ради их защиты, производительность их компонентов снижается, можно наблюдать серьёзный разрыв между производительностью дорогих и дешёвых телефонов и планшетов. Время, необходимое на выполнение JS-кода, важно для мобильных устройств с медленными процессорами. Это сильно влияет на производительность JavaScript-кода, так как возможности по выполнению неким устройством такого кода ограничены возможностями процессора этого устройства.

Ниже показан анализ загрузки весьма типичной по составу веб-страницы (reddit.com) на высокопроизводительном настольном компьютере. На самом деле, если проанализировать общее время, тратящееся на загрузку и подготовку к работе страницы в браузере наподобие Chrome, то около 30% этого времени может быть потрачено на выполнение JS-кода.

В процессе загрузки страницы около 10-30% времени тратится на выполнение кода средствами V8

На слабом устройстве (Alcatel 1X стоимостью менее $100) на решение той же задачи требуется, как минимум, в 6 раз больше времени, чем на чём-то вроде Pixel 3. Если говорить о мобильных устройствах, то на среднем телефоне (Moto G4) на выполнение JS-кода reddit.com уходит в 3-4 раза больше времени, чем на устройстве высокого уровня (Pixel 3).

Время, необходимое на обработку JS-кода на мобильных устройствах разных классов

Поэтому здесь нельзя сравнивать результаты мобильных устройств и, скажем, MacBook Pro. Обратите внимание на то, что мобильная и настольная версии reddit.com различаются.

Эти задачи могут препятствовать выполнению других, чрезвычайно важных задач, даже тогда, когда внешне страница выглядит полностью готовой к работе. Когда вы пытаетесь оптимизировать время выполнения JavaScript-кода — обращайте внимание на длительные задачи, которые могут надолго захватывать UI-поток. Деля код на части и управляя порядком загрузки этих частей можно добиться того, что страницы быстрее будут приходить в интерактивное состояние. Длительные задачи стоит разбивать на более мелкие задачи. Это, можно надеяться, приведёт к тому, что у пользователей будет меньше неудобств во взаимодействии со страницами.

Длительные задачи захватывают главный поток. Их стоит разбивать на части

Как улучшения V8 влияют на ускорение разбора и компиляции скриптов?

Скорость синтаксического анализа исходного JS-кода в V8, со времён Chrome 60, повысилась в 2 раза. В то же время, парсинг и компиляция теперь вносят меньший вклад в «цену JavaScript». Это так благодаря другим работам по оптимизации Chrome, ведущим к параллелизации выполнения этих задач.

Например, для Facebook улучшение этого показателя составило 46%, для Pinterest — 62%. В V8 объём работ по разбору и компиляции кода, производимых в главном потоке, снижен в среднем на 40%. Такие результаты возможны благодаря тому, что парсинг и компиляция вынесены в отдельный поток. Наиболее высокий результат, составляющий 81%, получен для YouTube. И это — вдобавок к уже существующим улучшениям, касающимся потокового решения тех же задач за пределами главного потока.

Время парсинга JS в различных версиях Chrome

За то же время, которое Chrome 61 нужно было для парсинга JS-кода Facebook, Chrome 75 теперь может разобрать JS-код Facebook и, вдобавок, 6 раз разобрать код Twitter. Ещё можно визуализировать то, как оптимизации V8, производимые в различных версиях Chrome, воздействуют на процессорное время, необходимое для обработки кода.

За то время, которое Chrome 61 было нужно для обработки JS-кода Facebook, Chrome 75 может обработать и код Facebook, и шестикратный объём кода Twitter

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

  • V8 может выполнять парсинг и компиляцию JS-кода не блокируя главный поток.
  • Потоковая обработка скрипта начинается с того момента, когда универсальный HTML-парсер встречает тег <script>. HTML-парсер выполняет обработку скриптов, блокирующих разбор страницы. Встречаясь с асинхронными скриптами он продолжает работу.
  • В большинстве реальных сценариев, характеризующихся определёнными скоростями сетевых подключений, V8 разбирает код быстрее, чем он успевает загружаться. В результате V8 завершает задачи по парсингу и компиляции кода через несколько миллисекунд после того, как будут загружены последние байты скрипта.

Если рассказать об этом всём немного подробнее, то дело тут заключается в следующем. В гораздо более старых версиях Chrome скрипт, прежде чем можно было бы приступить к его парсингу, требовалось загрузить целиком. Этот подход отличается простотой и понятностью, но при его применении нерационально используются ресурсы процессора. Chrome, между версиями 41 и 68, начинает парсинг в асинхронном режиме, сразу после начала загрузки скрипта, выполняя эту задачу в отдельном потоке.

Скрипты поступают в браузер фрагментами. V8 начинает потоковую обработку данных после того, как у него будет хотя бы 30 Кб кода.

Здесь планировщик может одновременно запустить несколько сеансов асинхронной/отложенной обработки скриптов. В Chrome 71 мы перешли к системе, основанной на задачах. Это привело примерно к 2% улучшению показателей TTI/FID, полученных на реальных сайтах. Благодаря этому изменению нагрузка, создаваемая парсингом на главный поток, снизилась примерно на 20%.

В Chrome 71 используется система обработки кода, основанная на задачах. При таком подходе планировщик может обрабатывать несколько асинхронных/отложенных скриптов одновременно

Теперь так обрабатываются даже обычные синхронные скрипты (хотя это не относится к встроенным скриптам). В Chrome 72 мы сделали потоковую обработку основным способом парсинга скриптов. Сделано это из-за того, что это приводит к необходимости повторного выполнения некоторой части уже сделанной работы. Кроме того, мы перестали отменять операции синтаксического анализа, основанные на задачах, в том случае, если главный поток нуждается в разбираемом коде.

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

Происходило это из-за того, что главный поток мог быть занят какими-то другими задачами (вроде разбора HTML, формирования макета страницы или выполнения JS-кода). Это часто приводило к тому, что потоковому синтаксическому анализатору приходилось ждать данных, которые уже загружены из сети, но ещё не перенаправлены главным потоком на потоковую обработку.

Ранее реализации подобного механизма мешала необходимость использования ресурсов главного потока для передачи заданий потоковому парсеру. Теперь мы экспериментируем над тем, чтобы начинать парсинг кода при предварительной загрузке страниц. Подробности о разборе JS-кода, который выполняется «мгновенно», можно узнать здесь.

Как улучшения повлияли на то, что можно видеть в инструментах разработчика?

В дополнение к вышесказанному можно отметить, что в инструментах разработчика раньше была одна проблема. Она заключалась в том, что сведения о задаче синтаксического анализа выводились так, будто бы они полностью блокируют главный поток. Однако парсер выполнял операции, блокирующие главный поток, только в том случае, когда он нуждался в новых данных. С тех пор, как мы перешли от схемы использования одного потока для потоковой обработки данных к схеме, в которой применяются задачи потоковой обработки, это стало совершенно очевидным. Вот что можно было видеть в Chrome 69.

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

08 секунды. Тут можно видеть, что задача «Parse Script» занимает 1. Большую часть этого времени не выполняется ничего полезного за исключением ожидания данных от главного потока.
В Chrome 76 можно видеть уже совсем другую картину. Но парсинг JavaScript, на самом деле, не является настолько медленным!

В Chrome 76 процедура парсинга разбита на множество мелких задач

Для того чтобы получить более детальные сведения, отражающие особенности V8, такие, как время разбора и время компиляции, можно воспользоваться средством Chrome Tracing с поддержкой RCS (Runtime Call Stats). В целом можно отметить, что вкладка Performance инструментов разработчика отлично подходит для того, чтобы увидеть общую картину того, что происходит на странице. Они способны сообщить о том, сколько времени ушло на парсинг и компиляцию JS-кода за пределами главного потока. В полученных RCS-данных можно найти показатели Parse-Background и Compile-Background. Показатели Parse и Compile указывают на то, сколько времени на соответствующие действия затрачено в главном потоке.

Анализ RCS-данных средствами Google Tracing

Как изменения отразились на работе с реальными сайтами?

Рассмотрим несколько примеров того, как потоковая обработка скриптов повлияла на просмотр реальных сайтов.

▍Reddit

Просмотр сайта reddit.com на MacBook Pro. Время на парсинг и компиляцию JS-кода, потраченное в главном и в рабочем потоках

Они обёрнуты внешними функциями, что приводит к выполнению больших объёмов «ленивой» компиляции в главном потоке. На сайте reddit.com имеется несколько JS-бандлов, размер каждого из которых превышает 100 Кб. Это так из-за того, что большая нагрузка на главный поток может увеличить время, которое нужно странице для перехода в интерактивный режим. Решающее значение на вышеприведённой схеме имеет время, необходимое на обработку скриптов в главном потоке. При обработке кода сайта reddit.com основное время тратится в главном потоке, а ресурсы рабочего/фонового потока используются по минимуму.

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

▍Facebook

Просмотр сайта facebook.com на MacBook Pro. Время на парсинг и компиляцию JS-кода, потраченное в главном и в рабочем потоках

Этот код загружается с помощью примерно 292 запросов. Ещё мы можем рассмотреть сайт наподобие facebook.com, на котором используется около 6 Мб сжатого JS-кода. Большинство скриптов Facebook отличаются маленькими размерами и узкой направленностью. Некоторые из них асинхронны, некоторые направлены на предварительную загрузку данных, некоторые имеют низкий приоритет. Дело в том, что множество небольших скриптов можно одновременно разбирать и компилировать средствами потоковой обработки скриптов. Это способно хорошо отразиться на параллельной обработке данных средствами фоновых/рабочих потоков.

У вас, наверняка, нет приложений, которые долго держат открытыми (вроде того, что представляет собой сайт Facebook или интерфейс Gmail), и при работе с которыми может быть оправдана загрузка столь серьёзных объёмов скриптов настольным браузером. Обратите внимание на то, что ваш сайт, вероятно, отличается от сайта Facebook. Она заключается в том, что код приложения стоит разбивать на бандлы скромных размеров, и в том, что загружать эти бандлы нужно только тогда, когда в них возникает необходимость. Но, несмотря на это, мы можем дать общую рекомендацию, справедливую для любых проектов.

Когда главный поток чем-то занят, страница не может реагировать на воздействия пользователя. Хотя большая часть работ по синтаксическому анализу и компиляции JS-кода может быть проведена с помощью потоковых средств в фоновом потоке, для выполнения некоторых операций всё ещё требуется главный поток. Поэтому рекомендуется обращать внимание на то, какое воздействие на UX сайтов оказывают загрузка и выполнение JS-кода.

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

Цена парсинга JSON

Синтаксический анализ JSON-кода может быть гораздо эффективнее разбора JavaScript-кода. Дело в том, что грамматика JSON гораздо проще грамматики JavaScript. Это знание можно применить в целях улучшения скорости подготовки к работе веб-приложений, которые используют большие конфигурационные объекты (такие, как хранилища Redux), структура которых напоминает JSON-код. В результате оказывается, что вместо представления данных в виде встроенных в код объектных литералов можно представить их в виде строк JSON-объектов и распарсить эти объекты во время выполнения программы.

Первый подход, с использованием JS-объектов, выглядит так:

const data = ; // медленно

Второй подход, с применением JSON-строк, подразумевает использование таких конструкций:

const data = JSON.parse('{"foo":42,"bar":1337}'); // быстро

Так как обработку JSON-строк приходится выполнять лишь один раз, подход, в котором используется JSON.parse, оказывается гораздо быстрее, чем использование объектных литералов JavaScript. Особенно — при «холодной» загрузке страницы. Рекомендуется использовать JSON-строки для представления объектов, размеры которых начинаются от 10 Кб. Однако, как и в случае с любым советом, касающимся производительности, этому совету не стоит следовать бездумно. Прежде чем применять эту технику представления данных в продакшне, нужно произвести измерения и оценить её реальное воздействие на проект.

Речь идёт о том, что есть риск того, что такие литералы могут быть обработаны дважды: Использование объектных литералов в качестве хранилищ больших объёмов данных таит в себе ещё одну угрозу.

  1. Первый проход обработки выполняется при предварительном парсинге литерала.
  2. Второй подход выполняется в ходе «ленивого» парсинга литерала.

От первого прохода обработки объектных литералов избавиться нельзя. Но, к счастью, второго прохода можно избежать, размещая объектные литералы на верхнем уровне или внутри PIFE.

Что можно сказать о парсинге и компиляции кода при повторных посещениях сайтов?

Оптимизировать производительность сайта для тех случаев, когда пользователи посещают их больше одного раза, можно благодаря возможностям V8 по кэшированию кода и байт-кода. Когда скрипт запрашивается с сервера в первый раз, Chrome загружает его и передаёт V8 для компиляции. Браузер, кроме того, сохраняет файл этого скрипта в своём дисковом кэше. Когда выполняется второй запрос на загрузку этого же JS-файла, Chrome берёт его из браузерного кэша и снова передаёт V8 для компиляции. В этот раз, однако, скомпилированный код сериализуется и присоединяется к кэшированному файлу скрипта в виде метаданных.

Работа системы кэширования кода в V8

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

Итоги

В 2019 году главными узкими местами производительности веб-страниц являются загрузка и выполнение скриптов. Для того чтобы улучшить ситуацию — стремитесь к использованию синхронных (встроенных) скриптов маленьких размеров, которые нужны для организации взаимодействия пользователя с той частью страницы, которая видна ему сразу после загрузки. Скрипты, используемые для обслуживания других частей страниц, рекомендуется загружать в отложенном режиме. Разбивайте крупные бандлы на небольшие части. Это облегчит реализацию стратегии работы с кодом, при применении которой код загружается только тогда, когда он нужен, и только там, где он нужен. Это позволит по максимуму задействовать возможности V8, направленные на параллельную обработку кода.

Эта рекомендация вытекает из того факта, что мобильные устройства обычно работают в достаточно медленных сетях. Если вы разрабатываете мобильные проекты, то стоит стремиться к тому, чтобы в них использовалось бы как можно меньше JS-кода. Постарайтесь найти баланс между временем, необходимым на подготовку к работе скриптов, загружаемых из сети, и использованием кэша. Такие устройства, кроме того, могут быть ограничены в плане доступной оперативной памяти и имеющихся у них процессорных ресурсов. Это позволит максимизировать объём работ по парсингу и компиляции кода, выполняемых за пределами главного потока.

Уважаемые читатели! Оптимизируете ли вы свои веб-проекты с учётом особенностей обработки JS-кода современными браузерами?

Теги
Показать больше

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

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

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

Кнопка «Наверх»
Закрыть