[Перевод] Цена JavaScript в 2019 году
За последние несколько лет в том, что называют «ценой JavaScript», наблюдаются серьёзные положительные изменения благодаря повышению скорости парсинга и компиляции скриптов браузерами. Сейчас, в 2019 году, главными составляющими нагрузки на системы, создаваемой JavaScript, являются время загрузки скриптов и время их выполнения.
Взаимодействие пользователя с сайтом может быть временно нарушено в том случае, если браузер занят выполнением JavaScript-кода. В результате можно сказать, что сильное позитивное воздействие на производительность сайтов может оказать оптимизация узких мест, связанных с загрузкой и выполнением скриптов.
Общие практические рекомендации по оптимизации сайтов
Что вышесказанное означает для веб-разработчиков? Дело тут в том, что затраты ресурсов на парсинг (разбор, синтаксический анализ) и компиляцию скриптов уже не так серьёзны, как раньше. Поэтому при анализе и оптимизации JavaScript-бандлов разработчикам стоит прислушаться к следующим трём рекомендациям:
- Стремитесь снизить время, необходимое на загрузку скриптов.
- Постарайтесь, чтобы ваши JS-бандлы имели бы небольшой размер. Особенно это важно для сайтов, рассчитанных на мобильные устройства. Использование маленьких бандлов улучшает время загрузки кода, снижает уровень использования памяти, уменьшает нагрузку на процессор.
- Старайтесь, чтобы весь код проекта не был бы представлен в виде одного большого бандла. Если размер бандла превышает примерно 50–100 Кб — разделите его на отдельные фрагменты небольшого размера. Благодаря HTTP/2-мультиплексированию одновременно может выполняться отправка нескольких запросов к серверу и обработка нескольких ответов. Это снижает нагрузку на систему, связанную с необходимостью выполнения дополнительных запросов на загрузку данных.
- Если вы работаете над мобильным проектом — постарайтесь, чтобы код был бы как можно меньшего размера. Эта рекомендация связана с невысокими скоростями передачи данных по мобильным сетям. Кроме того, стремитесь к экономному использованию памяти.
- Стремитесь снизить время, необходимое на выполнение скриптов.
- Избегайте использования длительных задач, которые способны на долгое время нагружать главный поток и увеличивать время, необходимое на то, чтобы страницы оказывались бы в состоянии, в котором с ними могут взаимодействовать пользователи. В текущих условиях выполнение скриптов, происходящее после того, как они оказываются загруженными, вносит основной вклад в «цену JavaScript».
- Не встраивайте большие фрагменты кода в страницы.
- Здесь стоит придерживаться следующего правила: если размер скрипта превышает 1 Кб — постарайтесь не встраивать его в код страницы. Одной из причин этой рекомендации является тот факт, что 1 Кб — это тот предел, после которого в Chrome начинает работать кэширование кода внешних скриптов. Кроме того, учитывайте то, что разбор и компиляция встроенных скриптов всё ещё выполняются в главном потоке.
Почему так важно время загрузки и выполнения скриптов?
Почему в современных условиях важно оптимизировать время загрузки и выполнения скриптов? Время загрузки скриптов чрезвычайно важно в ситуациях, когда с сайтами работают через медленные сети. Несмотря на то, что в мире всё сильнее распространяются сети 4G (и даже 5G), свойство NetworkInformation.effectiveType во многих случаях использования мобильных соединений с Интернетом демонстрирует показатели, находящиеся на уровне 3G-сетей или даже на более низких уровнях.
Время, необходимое на выполнение JS-кода, важно для мобильных устройств с медленными процессорами. Из-за того, что в мобильных устройствах используются различные CPU и GPU, из-за того, что при перегреве устройств, ради их защиты, производительность их компонентов снижается, можно наблюдать серьёзный разрыв между производительностью дорогих и дешёвых телефонов и планшетов. Это сильно влияет на производительность JavaScript-кода, так как возможности по выполнению неким устройством такого кода ограничены возможностями процессора этого устройства.
На самом деле, если проанализировать общее время, тратящееся на загрузку и подготовку к работе страницы в браузере наподобие Chrome, то около 30% этого времени может быть потрачено на выполнение JS-кода. Ниже показан анализ загрузки весьма типичной по составу веб-страницы (reddit.com) на высокопроизводительном настольном компьютере.
В процессе загрузки страницы около 10–30% времени тратится на выполнение кода средствами V8
Если говорить о мобильных устройствах, то на среднем телефоне (Moto G4) на выполнение JS-кода reddit.com уходит в 3–4 раза больше времени, чем на устройстве высокого уровня (Pixel 3). На слабом устройстве (Alcatel 1X стоимостью менее $100) на решение той же задачи требуется, как минимум, в 6 раз больше времени, чем на чём-то вроде Pixel 3.
Время, необходимое на обработку JS-кода на мобильных устройствах разных классов
Обратите внимание на то, что мобильная и настольная версии reddit.com различаются. Поэтому здесь нельзя сравнивать результаты мобильных устройств и, скажем, MacBook Pro.
Когда вы пытаетесь оптимизировать время выполнения JavaScript-кода — обращайте внимание на длительные задачи, которые могут надолго захватывать UI-поток. Эти задачи могут препятствовать выполнению других, чрезвычайно важных задач, даже тогда, когда внешне страница выглядит полностью готовой к работе. Длительные задачи стоит разбивать на более мелкие задачи. Деля код на части и управляя порядком загрузки этих частей можно добиться того, что страницы быстрее будут приходить в интерактивное состояние. Это, можно надеяться, приведёт к тому, что у пользователей будет меньше неудобств во взаимодействии со страницами.
Длительные задачи захватывают главный поток. Их стоит разбивать на части
Как улучшения V8 влияют на ускорение разбора и компиляции скриптов?
Скорость синтаксического анализа исходного JS-кода в V8, со времён Chrome 60, повысилась в 2 раза. В то же время, парсинг и компиляция теперь вносят меньший вклад в «цену JavaScript». Это так благодаря другим работам по оптимизации Chrome, ведущим к параллелизации выполнения этих задач.
В V8 объём работ по разбору и компиляции кода, производимых в главном потоке, снижен в среднем на 40%. Например, для Facebook улучшение этого показателя составило 46%, для Pinterest — 62%. Наиболее высокий результат, составляющий 81%, получен для YouTube. Такие результаты возможны благодаря тому, что парсинг и компиляция вынесены в отдельный поток. И это — вдобавок к уже существующим улучшениям, касающимся потокового решения тех же задач за пределами главного потока.
Время парсинга JS в различных версиях Chrome
Ещё можно визуализировать то, как оптимизации V8, производимые в различных версиях Chrome, воздействуют на процессорное время, необходимое для обработки кода. За то же время, которое Chrome 61 нужно было для парсинга JS-кода Facebook, Chrome 75 теперь может разобрать JS-код Facebook и, вдобавок, 6 раз разобрать код Twitter.
За то время, которое Chrome 61 было нужно для обработки JS-кода Facebook, Chrome 75 может обработать и код Facebook, и шестикратный объём кода Twitter
Поговорим о том, как были достигнуты подобные улучшения. Если в двух словах, то скриптовые ресурсы могут быть разобраны и скомпилированы в потоковом режиме в рабочем потоке. Это означает следующее:
- V8 может выполнять парсинг и компиляцию JS-кода не блокируя главный поток.
- Потоковая обработка скрипта начинается с того момента, когда универсальный HTML-парсер встречает тег
. HTML-парсер выполняет обработку скриптов, блокирующих разбор страницы. Встречаясь с асинхронными скриптами он продолжает работу.
- В большинстве реальных сценариев, характеризующихся определёнными скоростями сетевых подключений, V8 разбирает код быстрее, чем он успевает загружаться. В результате V8 завершает задачи по парсингу и компиляции кода через несколько миллисекунд после того, как будут загружены последние байты скрипта.
Если рассказать об этом всём немного подробнее, то дело тут заключается в следующем. В гораздо более старых версиях Chrome скрипт, прежде чем можно было бы приступить к его парсингу, требовалось загрузить целиком. Этот подход отличается простотой и понятностью, но при его применении нерационально используются ресурсы процессора. Chrome, между версиями 41 и 68, начинает парсинг в асинхронном режиме, сразу после начала загрузки скрипта, выполняя эту задачу в отдельном потоке.
Скрипты поступают в браузер фрагментами. V8 начинает потоковую обработку данных после того, как у него будет хотя бы 30 Кб кода.
В Chrome 71 мы перешли к системе, основанной на задачах. Здесь планировщик может одновременно запустить несколько сеансов асинхронной/отложенной обработки скриптов. Благодаря этому изменению нагрузка, создаваемая парсингом на главный поток, снизилась примерно на 20%. Это привело примерно к 2% улучшению показателей TTI/FID, полученных на реальных сайтах.
В Chrome 71 используется система обработки кода, основанная на задачах. При таком подходе планировщик может обрабатывать несколько асинхронных/отложенных скриптов одновременно
В Chrome 72 мы сделали потоковую обработку основным способом парсинга скриптов. Теперь так обрабатываются даже обычные синхронные скрипты (хотя это не относится к встроенным скриптам). Кроме того, мы перестали отменять операции синтаксического анализа, основанные на задачах, в том случае, если главный поток нуждается в разбираемом коде. Сделано это из-за того, что это приводит к необходимости повторного выполнения некоторой части уже сделанной работы.
В предыдущей версии Chrome была поддержка потокового парсинга и потоковой компиляции кода. Тогда скрипт, загружаемый из сети, должен был сначала попасть в главный поток, а потом уже перенаправлялся в потоковую систему обработки скриптов.
Это часто приводило к тому, что потоковому синтаксическому анализатору приходилось ждать данных, которые уже загружены из сети, но ещё не перенаправлены главным потоком на потоковую обработку. Происходило это из-за того, что главный поток мог быть занят какими-то другими задачами (вроде разбора HTML, формирования макета страницы или выполнения JS-кода).
Теперь мы экспериментируем над тем, чтобы начинать парсинг кода при предварительной загрузке страниц. Ранее реализации подобного механизма мешала необходимость использования ресурсов главного потока для передачи заданий потоковому парсеру. Подробности о разборе JS-кода, который выполняется «мгновенно», можно узнать здесь.
Как улучшения повлияли на то, что можно видеть в инструментах разработчика?
В дополнение к вышесказанному можно отметить, что в инструментах разработчика раньше была одна проблема. Она заключалась в том, что сведения о задаче синтаксического анализа выводились так, будто бы они полностью блокируют главный поток. Однако парсер выполнял операции, блокирующие главный поток, только в том случае, когда он нуждался в новых данных. С тех пор, как мы перешли от схемы использования одного потока для потоковой обработки данных к схеме, в которой применяются задачи потоковой обработки, это стало совершенно очевидным. Вот что можно было видеть в Chrome 69.
Проблема в инструментах разработчика, из-за которой сведения о парсинге скриптов выводились так, будто они полностью блокируют главный поток
Тут можно видеть, что задача «Parse Script» занимает 1.08 секунды. Но парсинг JavaScript, на самом деле, не является настолько медленным! Большую часть этого времени не выполняется ничего полезного за исключением ожидания данных от главного потока.
В Chrome 76 можно видеть уже совсем другую картину.
В Chrome 76 процедура парсинга разбита на множество мелких задач
В целом можно отметить, что вкладка Performance инструментов разработчика отлично подходит для того, чтобы увидеть общую картину того, что происходит на странице. Для того чтобы получить более детальные сведения, отражающие особенности V8, такие, как время разбора и время компиляции, можно воспользоваться средством Chrome Tracing с поддержкой RCS (Runtime Call Stats). В полученных RCS-данных можно найти показатели Parse-Background и Compile-Background. Они способны сообщить о том, сколько времени ушло на парсинг и компиляцию JS-кода за пределами главного потока. Показатели Parse и Compile указывают на то, сколько времени на соответствующие действия затрачено в главном потоке.
Анализ RCS-данных средствами Google Tracing
Как изменения отразились на работе с реальными сайтами?
Рассмотрим несколько примеров того, как потоковая обработка скриптов повлияла на просмотр реальных сайтов.
Просмотр сайта reddit.com на MacBook Pro. Время на парсинг и компиляцию JS-кода, потраченное в главном и в рабочем потоках
На сайте reddit.com имеется несколько JS-бандлов, размер каждого из которых превышает 100 Кб. Они обёрнуты внешними функциями, что приводит к выполнению больших объёмов «ленивой» компиляции в главном потоке. Решающее значение на вышеприведённой схеме имеет время, необходимое на обработку скриптов в главном потоке. Это так из-за того, что большая нагрузка на главный поток может увеличить время, которое нужно странице для перехода в интерактивный режим. При обработке кода сайта reddit.com основное время тратится в главном потоке, а ресурсы рабочего/фонового потока используются по минимуму.
Оптимизировать этот сайт можно было бы, разделив некоторые большие бандлы на части (размером около 50 Кб каждая) и обойдясь без оборачивания кода в функции. Это позволило бы максимизировать параллельную обработку скриптов. В результате бандлы можно было бы одновременно парсить и компилировать в потоковом режиме. Это сократило бы нагрузку на главный поток при подготовке страницы к работе.
Просмотр сайта facebook.com на MacBook Pro. Время на парсинг и компиляцию JS-кода, потраченное в главном и в рабочем потоках
Ещё мы можем рассмотреть сайт наподобие facebook.com, на котором используется около 6 Мб сжатого JS-кода. Этот код загружается с помощью примерно 292 запросов. Некоторые из них асинхронны, некоторые направлены на предварительную загрузку данных, некоторые имеют низкий приоритет. Большинство скриптов Facebook отличаются маленькими размерами и узкой направленностью. Это способно хорошо отразиться на параллельной обработке данных средствами фоновых/рабочих потоков. Дело в том, что множество небольших скриптов можно одновременно разбирать и компилировать средствами потоковой обработки скриптов.
Обратите внимание на то, что ваш сайт, вероятно, отличается от сайта Facebook. У вас, наверняка, нет приложений, которые долго держат открытыми (вроде того, что представляет собой сайт Facebook или интерфейс Gmail), и при работе с которыми может быть оправдана загрузка столь серьёзных объёмов скриптов настольным браузером. Но, несмотря на это, мы можем дать общую рекомендацию, справедливую для любых проектов. Она заключается в том, что код приложения стоит разбивать на бандлы скромных размеров, и в том, что загружать эти бандлы нужно только тогда, когда в них возникает необходимость.
Хотя большая часть работ по синтаксическому анализу и компиляции JS-кода может быть проведена с помощью потоковых средств в фоновом потоке, для выполнения некоторых операций всё ещё требуется главный поток. Когда главный поток чем-то занят, страница не может реагировать на воздействия пользователя. Поэтому рекомендуется обращать внимание на то, какое воздействие на UX сайтов оказывают загрузка и выполнение JS-кода.
Учитывайте то, что сейчас не все JavaScript-движки и браузеры реализуют потоковую обработку скриптов и оптимизацию их загрузки. Но мы, несмотря на это, надеемся на то, что общие принципы оптимизации, изложенные выше, способны улучшить впечатления пользователей от работы с сайтами, просматриваемыми в любом из существующих браузеров.
Цена парсинга JSON
Синтаксический анализ JSON-кода может быть гораздо эффективнее разбора JavaScript-кода. Дело в том, что грамматика JSON гораздо проще грамматики JavaScript. Это знание можно применить в целях улучшения скорости подготовки к работе веб-приложений, которые используют большие конфигурационные объекты (такие, как хранилища Redux), структура которых напоминает JSON-код. В результате оказывается, что вместо представления данных в виде встроенных в код объектных литералов можно представить их в виде строк JSON-объектов и распарсить эти объекты во время выполнения программы.
Первый подход, с использованием JS-объектов, выглядит так:
const data = { foo: 42, bar: 1337 }; // медленно
Второй подход, с применением JSON-строк, подразумевает использование таких конструкций:
const data = JSON.parse('{"foo":42,"bar":1337}'); // быстро
Так как обработку JSON-строк приходится выполнять лишь один раз, подход, в котором используется JSON.parse
, оказывается гораздо быстрее, чем использование объектных литералов JavaScript. Особенно — при «холодной» загрузке страницы. Рекомендуется использовать JSON-строки для представления объектов, размеры которых начинаются от 10 Кб. Однако, как и в случае с любым советом, касающимся производительности, этому совету не стоит следовать бездумно. Прежде чем применять эту технику представления данных в продакшне, нужно произвести измерения и оценить её реальное воздействие на проект.
Использование объектных литералов в качестве хранилищ больших объёмов данных таит в себе ещё одну угрозу. Речь идёт о том, что есть риск того, что такие литералы могут быть обработаны дважды:
- Первый проход обработки выполняется при предварительном парсинге литерала.
- Второй подход выполняется в ходе «ленивого» парсинга литерала.
От первого прохода обработки объектных литералов избавиться нельзя. Но, к счастью, второго прохода можно избежать, размещая объектные литералы на верхнем уровне или внутри PIFE.
Что можно сказать о парсинге и компиляции кода при повторных посещениях сайтов?
Оптимизировать производительность сайта для тех случаев, когда пользователи посещают их больше одного раза, можно благодаря возможностям V8 по кэшированию кода и байт-кода. Когда скрипт запрашивается с сервера в первый раз, Chrome загружает его и передаёт V8 для компиляции. Браузер, кроме того, сохраняет файл этого скрипта в своём дисковом кэше. Когда выполняется второй запрос на загрузку этого же JS-файла, Chrome берёт его из браузерного кэша и снова передаёт V8 для компиляции. В этот раз, однако, скомпилированный код сериализуется и присоединяется к кэшированному файлу скрипта в виде метаданных.
Работа системы кэширования кода в V8
Когда скрипт запрашивается в третий раз, Chrome берёт из кэша и файл, и его метаданные, после чего передаёт V8 и то и другое. V8 десериализует метаданные и, в результате, может пропустить шаг компиляции. Кэширование кода срабатывает в том случае, если визиты на сайт выполняются в пределах 72 часов. Chrome, кроме того, использует стратегию жадного кэширования кода тогда, когда для кэширования скриптов используется сервис-воркер. Подробности о кэшировании кода можно почитать здесь.
Итоги
В 2019 году главными узкими местами производительности веб-страниц являются загрузка и выполнение скриптов. Для того чтобы улучшить ситуацию — стремитесь к использованию синхронных (встроенных) скриптов маленьких размеров, которые нужны для организации взаимодействия пользователя с той частью страницы, которая видна ему сразу после загрузки. Скрипты, используемые для обслуживания других частей страниц, рекомендуется загружать в отложенном режиме. Разбивайте крупные бандлы на небольшие части. Это облегчит реализацию стратегии работы с кодом, при применении которой код загружается только тогда, когда он нужен, и только там, где он нужен. Это позволит по максимуму задействовать возможности V8, направленные на параллельную обработку кода.
Если вы разрабатываете мобильные проекты, то стоит стремиться к тому, чтобы в них использовалось бы как можно меньше JS-кода. Эта рекомендация вытекает из того факта, что мобильные устройства обычно работают в достаточно медленных сетях. Такие устройства, кроме того, могут быть ограничены в плане доступной оперативной памяти и имеющихся у них процессорных ресурсов. Постарайтесь найти баланс между временем, необходимым на подготовку к работе скриптов, загружаемых из сети, и использованием кэша. Это позволит максимизировать объём работ по парсингу и компиляции кода, выполняемых за пределами главного потока.
Уважаемые читатели! Оптимизируете ли вы свои веб-проекты с учётом особенностей обработки JS-кода современными браузерами?