За последние годы instagram.com претерпел множество изменений - мы запустили истории, фильтры, инструменты для создания, уведомления и прямой обмен сообщениями, а также множество других функций и улучшений. Однако по мере роста продукта побочным эффектом было то, что наша веб-производительность стала снижаться. За последний год мы предприняли сознательные усилия, чтобы улучшить это. Эти постоянные усилия привели к общему сокращению времени загрузки страницы фида почти на 50%. В этой серии сообщений в блоге мы расскажем о проделанной нами работе, которая привела к этим улучшениям. В части 1 мы говорили о предварительной выборке данных, в части 2 мы говорили об улучшении производительности путем отправки данных непосредственно клиенту, а не ожидания, пока клиент запросит данные, а в части 3 мы говорили о кешировании. первый рендеринг.

Встроенный требует

Мы объединяем наши веб-ресурсы в пакеты с помощью Metro (тот же пакет, который используется в React Native), поэтому мы получаем доступ к встроенным требованиям прямо из коробки. Inline-requires перемещает стоимость запроса / импорта модулей до уровня в первый раз, когда они действительно используются. Это означает, что вы можете избежать оплаты затрат на выполнение за неиспользуемые функции (хотя вы все равно будете платить за их загрузку и анализ) и сможете лучше амортизировать затраты на выполнение на время запуска приложения, вместо того, чтобы иметь большой объем предварительных вычислений.

Обслуживание пакетов ES2017 в современных браузерах

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

При использовании встроенных требований это будет преобразовано во что-то вроде следующего (вы найдете эти встроенные требования, выполнив поиск r(d[ в исходном коде Instagram JS в инструментах разработчика браузера)

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

Без встроенных требований модуль C будет выводить {'foo':'bar'}, но когда мы включаем встроенные требования, он будет выводить undefined, потому что B имеет неявную зависимость от A. Это надуманный пример, но есть и другие реальные случаи, когда это может иметь последствия. то есть, что, если модуль выполняет какое-то протоколирование как часть своей инициализации - включение встроенных требований может привести к тому, что это ведение журнала прекратится. В основном это можно предотвратить с помощью линтеров, которые проверяют код, который выполняется немедленно на уровне области модуля, но были некоторые файлы, которые нам пришлось занести в черный список в результате этой оптимизации, например полифиллы времени выполнения, которые необходимо выполнить немедленно. После экспериментов с включением встроенных требований во всей кодовой базе мы увидели улучшение нашего Feed TTI (время до интерактивности) на 12% и Display Done на 9,8%, и решили, что работа с некоторыми из этих незначительных крайних случаев того стоит для повышения производительности.

Одним из основных драйверов, которые стимулировали внедрение инструментов компилятора / транспилятора, таких как Babel, было разрешение разработчикам использовать современные идиомы кодирования JavaScript, но при этом их приложения по-прежнему работают в браузерах, в которых отсутствует встроенная поддержка этих новейших языковых функций. С тех пор возник ряд других важных вариантов использования этих инструментов, включая языки компиляции в js, такие как Typescript и ReasonML, языковые расширения, такие как JSX и аннотации типов Flow, и манипуляции с AST во время сборки для таких вещей, как интернационализация. Из-за этого маловероятно, что этот дополнительный этап компиляции исчезнет из рабочих процессов фронтенд-разработки в ближайшее время. Однако с учетом вышесказанного стоит вернуться к рассмотрению, если первоначальная цель для этого (кроссбраузерная совместимость) все еще необходима в 2019 году.

ES2015 и более свежие функции, такие как async / await, теперь хорошо поддерживаются в последних версиях. большинства основных браузеров, поэтому прямое обслуживание JavaScript, содержащего эти новые функции, определенно возможно, но есть два ключевых вопроса, на которые мы должны сначала ответить:

Класс (ES2017 против ES5)

Чтобы ответить на первый вопрос, нам сначала нужно было определить, какие функции мы собираемся поставлять без транспиляции / полифиллинга и сколько вариантов сборки мы хотим поддерживать для разных браузеров. Мы остановились на двух сборках, одна из которых потребует поддержки синтаксиса ES2017, и устаревшая сборка, которая будет переноситься обратно в ES5 (кроме того, мы также добавили дополнительный пакет полифиллов, который будет добавлен только для устаревших браузеров, в которых отсутствует поддержка во время выполнения). последние DOM API). Обнаружение поддержки для этих групп осуществляется с помощью некоторого базового сниффинга пользовательского агента на стороне сервера, что гарантирует отсутствие затрат времени выполнения или дополнительного времени туда и обратно при обнаружении на стороне клиента того, какие пакеты загружать.

  • И каковы (если есть) преимущества в производительности от поставки функций ES2015 +?
  • В частях 1–3 мы рассмотрели различные способы оптимизации шаблонов загрузки статических ресурсов критического пути и запросов данных. Однако есть еще одна ключевая область, которую мы еще не рассмотрели и которая имеет решающее значение для повышения производительности веб-приложений, особенно на устройствах низкого уровня, - отправляйте меньше кода пользователю, в частности, отправляйте меньше JavaScript. .

    Это может показаться очевидным, но здесь следует учесть несколько моментов. В отрасли широко распространено предположение, что размер JavaScript, загружаемого по сети, является важным (т.е. размер после сжатия), однако мы обнаружили, что действительно важен размер предварительное сжатие, поскольку это то, что должно быть проанализировано и выполнено на устройстве пользователя, даже если оно кэшировано локально. Это особенно актуально, если у вас есть сайт с большим количеством повторных пользователей (и, соответственно, с высокой посещаемостью кеша браузера) или пользователей, обращающихся к вашему сайту с мобильных устройств. В этих случаях ограничивающим фактором становится производительность синтаксического анализа и выполнения JavaScript на ЦП, а не время сетевой загрузки. Например, когда мы реализовали сжатие «Brotli» для наших ресурсов JavaScript, мы увидели почти 20% уменьшение размера пост-сжатия по сети, но НЕТ статистически значимое уменьшение в общем времени загрузки страницы с точки зрения конечных пользователей.

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

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

Имея это в виду, мы провели подсчеты и определили, что 56% пользователей «instagram.com» могут получить сборку ES2017 без каких-либо транспиляций или полифиллов во время выполнения, и учитывая, что этот процент со временем будет только расти. - кажется, стоит поддерживать две сборки, учитывая количество пользователей, которые могут ее использовать.

Что касается второго вопроса - каковы преимущества производительности при отправке ES2017 напрямую - давайте начнем с рассмотрения того, что на самом деле делает Babel для переноса некоторых распространенных конструкций обратно в ES5. В левом столбце находится код ES2017, а справа - перенесенная версия, совместимая с ES5.

Из этого мы можем видеть, что при транспиляции этих конструкций возникают значительные накладные расходы (даже если вы амортизируете стоимость некоторых вспомогательных функций времени выполнения по большой базе кода). В случае с Instagram мы увидели уменьшение размера наших основных потребительских пакетов JavaScript на 5,7%, когда мы удалили все транспилируемые плагины ES2017 из нашей сборки. При тестировании мы обнаружили, что время сквозной загрузки для страницы канала улучшилось на 3% для пользователей, которым был предоставлен пакет ES2017, по сравнению с теми, кто этого не делал.

Async / Await (ES2017 против ES5)

Стрелочные функции (ES2017 против ES5)

Остальные параметры (ES2017 против ES5)

Назначение деструктуризации (ES2017 против ES5)

Еще долгий путь

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

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

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

Ускорение instagram.com: оптимизация размера кода и выполнения (часть 4)