DevGang
Авторизоваться

Архитектура интерфейса производительности

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

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

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

Обзор

Я сгруппирую загрузку приложения в три разных этапа:

  1. Первоначальный рендеринг - сколько времени нужно, прежде чем пользователь что-либо увидит?
  2. Загрузка приложения - сколько времени нужно, прежде чем пользователь сможет использовать приложение?
  3. Следующая страница - сколько времени нужно, чтобы перейти на следующую страницу?

Начальный рендер

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

В этом посте я буду использовать каскадные диаграммы WebPageTest. Каскад запросов для вашего сайта, вероятно, будет выглядеть примерно так.

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

Уменьшите количество запросов на блокировку рендеринга

Таблицы стилей и (по умолчанию) элементы скрипта блокируют отображение любого содержимого под ними.

У вас есть несколько способов исправить это:

  1. Поместите теги скрипта внизу тега body
  2. Загружать скрипты асинхронно с async
  3. Встроить небольшие фрагменты JS или CSS, если их нужно загружать синхронно.

Избегайте последовательных цепочек запросов, блокирующих рендеринг

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

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

  1. Правила @import в CSS
  2. Веб-шрифты, на которые есть ссылки в файле CSS
  3. Ссылка на внедрение JavaScript или теги скрипта

Взгляните на этот пример:

Этот веб-сайт использует @import в одном из своих файлов CSS для загрузки шрифта Google. Это означает, что браузеру необходимо выполнять эти запросы один за другим:

  1. Документ HTML
  2. CSS приложения
  3. Google шрифты CSS
  4. Файл Google Font Woff (не показан в каскаде)

Чтобы исправить это, сначала переместите запрос в CSS Google Fonts из @import в тег ссылки в HTML-документе. Это отрезает одно звено цепи.

Чтобы еще больше ускорить процесс, вставьте CSS- файл Google Fonts прямо в свой HTML-код или в свой CSS-файл.

(Имейте в виду, что ответ CSS от Google Fonts зависит от пользовательского агента. Если вы сделаете запрос с IE8, CSS будет ссылаться на файл EOT, IE11 получит файл woff, а современные браузеры получат файл woff2. Но если вы в порядке со старыми браузерами, использующими системные шрифты, тогда вы можете просто скопировать и вставить содержимое файла CSS.)

Даже после того, как страница начинает рендеринг, пользователь по-прежнему может не иметь возможности что-либо делать со страницей, потому что текст не будет отображаться, пока шрифт не будет загружен. Этого можно избежать с помощью замены отображения шрифтов, которую Google Fonts теперь использует по умолчанию.

Иногда устранить цепочку запросов невозможно. В этих случаях вы можете использовать тег preload или preconnect. Например, указанный выше веб-сайт может подключиться к fonts.googleapis.com до того, как будет сделан фактический запрос CSS.

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

Установление нового соединения с сервером обычно занимает 3 цикла между браузером и сервером:

  1. Поиск DNS
  2. Установление TCP-соединения
  3. Установление SSL-соединения

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

Приведенный ниже каскад показывает, что соединения инициируются с четырьмя разными серверами: hostgator.com, optimizely.com, googletagmanager.com и googelapis.com.

Однако последующие запросы к тому же серверу могут повторно использовать существующее соединение. Таким образом, загрузка base.css или index1.css выполняется быстро, потому что они также размещены на hostgator.com.

Уменьшите размер файла и используйте CDN

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

Отправьте пользователю как можно меньше данных и убедитесь, что они сжаты (например, с помощью brotli или gzip).

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

Пропустите сеть с сервисными работниками

Сервис-работники позволяют вам перехватывать запросы до того, как они попадут в сеть. Это означает, что вы можете получить первую окраску практически мгновенно!

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

Сервисный работник ниже кэширует HTML и CSS, необходимые для отображения страницы. Когда приложение загружается снова, оно пытается обслуживать кэшированные ресурсы и возвращается в сеть, если они недоступны.

self.addEventListener("install", async e => {
 caches.open("v1").then(function (cache) {
   return cache.addAll(["/app", "/app.css"]);
 });
});

self.addEventListener("fetch", event => {
 event.respondWith(
   caches.match(event.request).then(cachedResponse => {
     return cachedResponse || fetch(event.request);
   })
 );
});

Загрузка приложения

Итак, теперь пользователь что-то видит. Что еще нужно сделать, прежде чем они смогут использовать ваше приложение?

  1. Загрузить код приложения (JS и CSS)
  2. Загрузить необходимые данные для страницы
  3. Загрузить дополнительные данные и изображения

Обратите внимание, что не только загрузка данных из сети может задержать рендеринг. После загрузки кода браузеру необходимо будет проанализировать, скомпилировать и выполнить его.

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

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

Обычно код разбивается на файлы трех разных типов:

  1. Код для конкретной страницы
  2. Общий код приложения
  3. Сторонние модули, которые редко меняются (отлично подходят для кеширования!)

Webpack может автоматически разделять общий код, чтобы уменьшить общий вес загрузки, используя optim.splitChunks. Обязательно включите фрагмент времени выполнения, чтобы хэши фрагментов были стабильными и вы получали выгоду от длительного кэширования.

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

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

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

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

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

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

Загрузка данных страницы

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

Не ждите пакетов, прежде чем начать загрузку данных

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

Избежать этого можно двумя способами:

  1. Вставить данные страницы в HTML-документ
  2. Запустите запрос данных с помощью встроенного скрипта внутри документа.

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

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

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

window.userDataPromise = fetch("/me")

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

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

Не блокируйте рендеринг, ожидая второстепенных данных

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

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

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

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

Вместо того, чтобы сначала делать запрос о том, под каким именем зарегистрирован пользователь, а затем запрашивать список команд, к которым они принадлежат, верните список команд вместе с информацией о пользователе. Для этого вы можете использовать GraphQL, но и настраиваемая  конечная точка user?includeTeams=true тоже отлично работает.

Серверный рендеринг

Рендеринг на стороне сервера означает предварительный рендеринг вашего приложения на сервере и ответ на запрос документа полным HTML-кодом страницы. Это означает, что клиент может видеть страницу полностью отрисованной, не дожидаясь загрузки дополнительного кода или данных!

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

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

Следующая страница

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

Предварительная загрузка ресурсов

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

import(
    /* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList"
)

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

Стратегически определите, какие части вашего приложения, скорее всего, понадобятся пользователю.

Повторно использовать уже загруженные данные

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

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

Вывод

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

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

При написании этого поста я понял одну вещь: у меня было укоренившееся убеждение, что выполнение множества отдельных запросов плохо сказывается на производительности. Так было в прошлом, когда для каждого запроса требовалось отдельное соединение, а браузеры разрешали только несколько соединений на домен. Но с HTTP / 2 и современными браузерами это уже не так.

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

Источник:

#Optimisation
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Оплатив хостинг 25$ в подарок вы получите 100$ на счет

Получить