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

Введение

Читатели моего последнего поста высоко оценили обновления во время выполнения, но отметили неприемлемость показателей Lighthouse. Поэтому я исправил показатели Lighthouse (объяснение ниже). Меня также попросили добавить контекст, касающийся типа веб-приложений в Play, а также таких вопросов, как SSR и гидратация.

Итак, рассмотрим два очень распространенных, но разных типа приложений.

  1. Сайты и интернет-магазины

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

  1. Корпоративные приложения и социальные сети

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

Я разработал приложение Neo для создания полнофункциональных интернет-приложений (RIA) с мощными/полезными клиентами на стороне браузера (оптимизация относится ко 2-ой категории). Обратите внимание, что интерактивные дэшборд-приложения — практически образчики высокопроизводительных RIA/мощных клиентов.

Что касается быстрых обновлений и показателей Lighthouse, то подход с Neo заключается в следующем:

  • выполнение начальной загрузки страницы без html-контента — начиная с пустого тега body и без непосредственно включенных таблиц стилей;
  • немедленная загрузка на клиент небольших html-генераторов JavaScript — браузер может их кэшировать, так что их не нужно будет повторно загружать;
  • отправка JSON-данных конфигурации и JSON- или других данных контента, впоследствии используемых генераторами для заполнения и обновления DOM… контента этого тега body.

Стандартный файл index.html для приложений Neo.mjs:

<!DOCTYPE HTML>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="UTF-8">
    <title>My App</title>
</head>
<body>
    <script src="../../src/MicroLoader.mjs" type="module"></script>
</body>
</html>

Клиент Neo — собранные генераторы JavaScript — располагает всеми инструментами для создания сложных, динамических пользовательских интерфейсов. Не нужно передавать HTML с сервера (он же SSR) и разбираться со сложностями гидратации.

Подсказка: такой подход может сэкономить много трафика, а значит, и денег.

Целевая страница Neo.mjs 
Обучающий раздел Neo.mjs 

Что особенного в этом приложении?

В предыдущей статье я показал режим разработки приложения. Данная версия содержит самый свежий код ECMAScript, работающий непосредственно в браузере. Никаких компиляций или транспиляций не требуется. Очевидно, что этот код не был минифицирован.

Чтобы развернуть приложение, нужно отправить версию dist/production, которая минифицируется и пакетируется с помощью таких инструментов, как Webpack, ESBuild и Vite.

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

В обучающем разделе и на целевой странице Neo.mjs мы намерены применить редактор Monaco (также используемый в VSCode). Этот «зверь» имеет размер файла 3MB+, что делает достижение высоких показателей Lighthouse не совсем простым делом.

code.LivePreview: представление исходного кода

В этом редакторе кода можно импортировать любые классы (модули) Neo.mjs.

code.LivePreview: представление предварительного просмотра

Сгенерированный код не помещается в iFrame, а напрямую встраивается в DOM. Таким образом, импортированные модули JavaScript находятся в той же области, что и основное приложение.

Примечание: речь идет не о главном потоке, а о SharedWorker (общем воркере приложения).

code.LivePreview: вывод DOM

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

Итак, нам необходимо, чтобы основное приложение работало в минифицированном режиме dist/production, а все сгенерированные модули code.LivePreview — в некомпилированном режиме development. Кроме того, обе версии должны работать в одном воркере и напрямую взаимодействовать. Скоро углубимся в этот вопрос.

Высокоинтерактивные многооконные представления

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

Демонстрация компонентов управления Helix в 1-м окне 
Расширенная демонстрация компонентов управления Helix во 2-м окне
Дэшборд SocketConnection в 1-м окне
Дэшборд SocketConnection, отделенный от LivePreview
Дэшборд SocketConnection с 2 отдельными окнами

Стоит отметить, что мы даже не создаем новые экземпляры виджетов — просто перемещаем DOM-вывод одних и тех же JavaScript-экземпляров в разные окна браузера. Это также приведет к delta-обновлению CSS в разных окнах.

Ссылки на онлайн-демонстрации

Сначала немного краткой информации:

  • Чтобы в полной мере насладиться многооконными демонстрациями, в идеале открывайте приложение в десктопной версии браузера.
  • Убедитесь, что включили всплывающие окна на странице (отсутствие рекламы гарантирую).
  • Многооконность также работает на устройствах Android при использовании Chrome.
  • Приложение работает на iOS без многооконных функций.

Версия dist/production (та, которую необходимо проверить на производительность).

Версия development (на случай, если будет интересно погрузиться в неминифицированный код).

Рекомендация: чтобы просмотреть SharedWorkers, нужно открыть chrome://inspect/#workers.

Особое внимание обратите на навигацию внутри обучающего раздела (Learning Section): она теперь работает просто умопомрачительно.

Исходный код приложения можно найти здесь (полная лицензия от MIT).

Можно отключить режим «cube layout » (перейдя на режим «card layout») в будущих версиях. Просто оставим его в качестве сюрприза, включаемого во время выполнения.

Как запустить версии development и dist/production одного приложения на одной странице?

До сих пор в Neo.mjs версии 6.x это было просто невозможно.

Когда несколько версий или сред одного и того же фреймворка запускаются на одной странице, могут произойти довольно неприятные вещи. Представьте несколько версий IdGenerator, в результате чего DOM-идентификаторы перестают быть уникальными.

Подумайте и об использовании SharedWorkers, указывающих на разные файлы → они перестают быть «shared» (общими).

Рассмотрим очень простой класс Neo.mjs, чтобы увидеть изменения:

import Component from './Base.mjs';

class Label extends Component {
    static config = {
        className: 'Neo.component.Label'
        // ...
    }
}

Neo.setupClass(Label)

export default Label;

setupClass() применяет все статические конфигурации, агрегированные в цепочке прототипов, и возвращается скорректированный класс Label.

Новая версия:

import Component from './Base.mjs';

class Label extends Component {
    static config = {
        className: 'Neo.component.Label'
        // ...
    }
}

export default Neo.setupClass(Label);

Экспортируем не сам класс Label, а результат работы метода setupClass(). Теперь этот метод будет проверять, есть ли уже контент внутри заданного пространства имен className, и если да, то возвращать вместо него «кэшированную» версию.

Это изменение невероятно мощное и при этом такое простое.

Еще одно изменение было необходимо, чтобы обеспечить повторное использование одних и тех же экземпляров SharedWorker: при открытии нового окна браузера нужно проверять среду, в которой запущен Application Worker.

Иными словами, приложение Website App запущено в режиме dist/production, а Helix внутри LivePreview работает в режиме development. Таким образом, при открытии окна для отсоединения элементов управления, окно должно открыть файл/url Worker в режиме dist/production, даже если оно может подтягивать модули из режима development.

Есть ли лучшая альтернатива Lighthouse для измерения производительности навигации?

Недавно Chrome выпустил API Long Animation Frame (API длинных кадров анимации).

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

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

Можно ли еще больше повысить производительность этого приложения?

Можно, и мы работаем над этим.

Следующие шаги:

  1. Пакетирование активов (элемент бэклога промежуточного ПО).
  1. Предварительная загрузка активов для критического пути рендеринга (также элемент бэклога промежуточного ПО).
  1. Еще большее усиление связи между воркерами.
  1. Исследование дерева на предмет delta-обновлений (возможно, в следующей основной версии).

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Tobias Uhlig: Frontend Performance Love Story

Предыдущая статья3 лайфхака по работе с музыкальным ИИ в условиях нехватки данных 
Следующая статьяРабочий процесс на GitHub: профессиональный уровень