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

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

Современные фронтенд-приложения становятся все больше и больше. Не удивительно, что индустрия серьёзно озадачилась вопросом оптимизации. При использовании фреймворков мы получаем неадекватные размеры приложения, которые могут пойти ему не на пользу, а во вред. Любой необязательный фрагмент JavaScript, который вы собираете и отправляете в продакшен, в итоге только увеличит количество кода, который клиенту нужно загрузить и обработать. Здесь дежурное правило гласит — чем меньше, тем лучше.

Шаблоны загрузки данных являются важнейшей частью вашего приложения, поскольку определяют, какие его части являются непосредственно доступными для использования посетителями. Не создавайте сайт, который будет тормозить у клиентов из-за необходимости скачать изображение размером 5МБ на главной странице. Старайтесь лучше понять принципы. Вам нужно хорошо разобраться в водопаде загрузки ресурсов.

Перебор со спиннерами загрузки и водопад данных


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

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


Домашняя страница Medium

Тут у нас два важных компонента:

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

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

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

Но разве ожидание данных не является само-собой разумеющимся?

Да, но их загрузку можно ускорить.



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

  • данных от API бэкенда;
  • создания макета согласно этим данным.

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

Пять важных шаблонов загрузки данных


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

Вы здесь, чтобы оптимизировать. Помните, чем меньше, тем лучше.

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

Рендеринг на стороне сервера и Jamstack


В современных JS-фреймворках отрисовка страниц зачастую реализуется на клиентской стороне (CSR). Браузер получает в виде полезной нагрузки JS-бандл и статический HTML, после чего отрисовывает DOM, а также для реактивности добавляет слушателей и активаторы событий. Когда такое приложение отрисовывается в DOM, страница блокируется до полного завершения этого процесса. Рендеринг делает приложение реактивным. Для его выполнения необходимо сделать ещё один вызов API к серверу и извлечь все нужные данные.

В свою очередь, отрисовка на стороне сервера (SSR) подразумевает передачу приложением простого HTML клиенту. SSR можно разделить на два типа: с гидратацией и без гидратации. SSR – это старая техника, используемая такими фреймворками, как WordPress, Ruby on Rails и ASP.NET. Основная её задача – предоставить пользователю статический HTML с необходимыми данными. В отличие от CSR, SSR не требуется совершать дополнительный вызов API к серверу, поскольку именно сервер генерирует шаблон HTML и загружает данные.

В более современных решениях вроде Next.js используется гидратация, при которой статический HTML гидратируется на клиентской стороне с помощью JS. Можно сравнить это с завариванием быстрорастворимого кофе: кофейный порошок – это HTML, а вода – это JS. При смешивании порошка с водой вы, очевидно, получите кофе.

Но что такое Jamstack? Jamstack аналогичен SSR, поскольку клиент получает простой HTML. Но во время SSR клиент извлекает HTML с сервера, а в случае с Jamstack приложения передают заранее сгенерированный HTML прямо из CDN. В связи с этим они обычно загружаются быстрее, но здесь разработчикам сложнее создавать динамический контент. Такие приложения за счёт предварительной генерации HTML хороши для клиента. Однако при использовании большого объёма JS-кода на стороне клиента становится всё сложнее оправдать использование Jamstack вместо СSR.

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


Jamstack в сравнении с SSR и CSR

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

SSR и Jamstack очень популярны, и всё больше проектов переходят на фреймоворки для SSR вроде Next.js и Nuxt.js, отказываясь от их ванильных CSR-аналогов – React и Vue. Главная причина в том, что SSR-фреймворки обеспечивают большую гибкость в отношении SEO. В Next.js есть целый раздел, ориентированный на SEO оптимизации.

Приложение с SSR обычно содержит шаблонизаторы, которые внедряют переменные в HTML при его передаче клиенту. К примеру, в Next.js можно загрузить список студентов, написав:

export default function Home({ studentList }) {
  return (
    <Layout home>
        <ul>
          {studentList.map(({ id, name, age }) => (
            <li key={id}>
              {name}
              <br />
              {age}
            </li>
          ))}
        </ul>
    </Layout>
  );
}

Jamstack популярен для сайтов документации, которые обычно компилируют код в HTML-файлы и размещают их в CDN. Файлы Jamstack, как правило, до компиляции в HTML используют разметку Markdown. Вот пример:

---
author: Agustinus Theodorus
title: 'Title'
description: Description
---
Hello World

Активное кэширование памяти


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

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

Но как делать правильный выбор между кэшем Redis (серверным) и кэшем браузера (локальным)? Оба этих варианта можно использовать одновременно, но каждый из них будет служить своей цели.


Схемы кэширования

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

В общем, если вы хотите повысить быстродействие приложения, то можете использовать для ускорения API серверный кэш. Если же вы хотите сохранять состояние приложения, то стоит прибегать к локальному. И хотя локальный кэш может показаться бесполезным, он помогает сократить число вызовов API к бэкенду, сохраняя состояние, которое меняется редко. Однако локальный кэш работает эффективнее при совмещении с «живыми» данными.

Генерация событий данных


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


Типичная архитектура Websocket

В стандартной архитектуре WebSocket фронтенд-приложение подключается к WebSocket API, шине событий или базе данных. В большинстве архитектур WebSocket это используется как альтернатива REST, особенно в приложениях вроде чатов, когда опрос бэкенда каждые несколько секунд становится неэффективен. Эта техника позволяет получать по двухстороннему каналу обновление с другой стороны — без создания нового запроса.

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


Архитектура генерации событий

В чистой реализации WebSocket по-прежнему есть множество недочётов. Использование этой технологии вместо стандартных HTTP-вызовов полностью изменит поведение приложения. Даже одна небольшая проблема с подключением сможет повлиять на весь пользовательский опыт. К примеру, WebSocket не может обеспечить быстродействие реального времени – когда требуется связаться с базой данных, используется запрос GET. Чтобы сделать WebSocket рациональным и целесообразным выбором в бэкенде необходимо устранить ряд слабых мест, препятствующих эффективной работе в режиме реального времени.

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

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

const ws = new WebSocket('ws://localhost');ws.addEventListener('message', (event) => {
    console.log('Message from server ', event.data);
});

Хотите реагировать на события сервера? Добавьте функцию addEventListener и вставьте обратный вызов, который она будет использовать.

ws.send('Hello World');

Хотите отправлять сообщение? И здесь у WebSocket есть решение. Отправку сообщений с сервера можно реализовать с помощью функции send. Причём это не сложнее вывода “Hello World”. Примеры взяты из документации MDN.

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


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

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


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

<link rel="prefetch" href="https://example.com/example.html">

URL для предварительной загрузки устанавливаются в атрибуте rel HTML-элемента link. Однако у данной техники есть как плюсы, так и минус.

Плюсы:

  1. Операция предварительной загрузки дожидается, пока сеть браузера не окажется свободной и останавливается, как только вы запустите некий сетевой процесс, кликнув по ссылке или активировав функцию отложенной загрузки.
  2. Предварительная загрузка кэширует данные в браузере, ускоряя переходы при перенаправлении по ссылке.

Минус:

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

▍ Отложенная загрузка


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


Отложенная загрузка

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

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

Resumability


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

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


Сравнение Resumability и гидатации. Изображение взято из Qwik

Загрузка страниц будет молниеносной, поскольку клиент вместо перезагрузки чего-либо будет десериализовывать состояние, внедрённое в HTML. Resumability является довольно чужеродной концепцией и для многих проектов покажется совсем непривычным приёмом. Придумал этот шаблон создатель Qwik, Миско Хевери.

Qwik – это JS-фреймворк, внутренне работающий по принципу Resumability, который в него закладывался изначально. Если же говорить о таких фреймворках, как React и Vue , то они никогда не смогут задействовать технику Resumability, не пожертвовав обратной совместимостью. Причина в том, что компонент отложенной загрузки в Qwik работает асинхронно в отличие от большинства синхронно устроенных фреймворков JavaScript.

Задача Qwik – загружать как можно меньше JS-кода. Дело в том, что делать это отложенно довольно трудно, а в некоторых случаях даже невозможно. Чем меньше вам нужно, тем лучше. Resumability позволяет разработчикам детально настроить отложенную загрузку и сократить потребление памяти мобильными приложениями, оптимизируя под них сайт.

Использование Qwik в некотором смысле напоминает React – в частности у них похож синтаксис. Вот фрагмент кода Qwik. Основа приложения будет представлена в форме HTML:

import { App } from './app';export const Root = () => {
  return (
    <html>
      <head>
        <title>Hello Qwik</title>
      </head>
      <body>
        <App />
      </body>
    </html>
  );
};

Здесь есть зависимость от App, компонента Qwik для отложенной загрузки:

import { component$ } from '@builder.io/qwik';export const App = component$(() => {
  return <p>Hello Qwik</p>;
});

У Qwik и React также есть сходства на уровне компонентов, а вот в серверной части они уже отличаются.

import { renderToString, RenderOptions } from '@builder.io/qwik/server';
import { Root } from './root';export default function (opts: RenderOptions) {
  return renderToString(<Root />, opts);
}

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

Обобщение


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

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

  1. Отрисовка на стороне сервера (SSR) и Jamstack.
  2. Активное кэширование памяти.
  3. Генерация событий данных.
  4. Предварительная и отложенная загрузка.
  5. Resumability.

Каждый из них эффективен на своём месте.

SSR и Jamstack обычно хорошо подходят для приложений, требующих меньшего управления состоянием на клиентской стороне. С появлением современных фреймворков вроде React всё больше людей познакомились с паттерном отрисовки на клиентской стороне (CSR), но в результате сообщество всё же возвращается к SSR. Техника SSR используется в старых MVC фреймворках, где с помощью шаблонизаторов на основе данных бэкенда генерируется HTML. Jamstack – это ещё более старая иллюстрация изначальной веб-среды, в которой использовался только HTML.

Активное кэширование памяти позволяет быстрее загружать данные из API. Эта техника решает важную проблему загрузки данных путём их кэширования либо на удалённом сервере (Redis), либо локально в браузере. Помимо этого, она используется в шаблоне предварительной загрзуки данных.

Что касается генерации событий, то этот архитектурный шаблон дополняет основанные на событиях WebSocket API, работающие в реальном времени. Простых старых WebSocket недостаточно для полноценной эффективности, поскольку, хоть сам WebSocket и работает в реальном времени, регулярные вызовы API к базе данных создают узкое место. Шаблон генерации событий устраняет эту проблему, создавая для загрузки данных отдельную БД.

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

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

Дальнейшие шаги


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

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

Благодарю за чтение. Надеюсь, статья была для вас полезной.

Играй в нашу новую игру прямо в Telegram!

Комментарии (2)


  1. gmtd
    08.01.2023 14:07
    +2

    Рассматривается вопрос ускорения загрузки сайта и нет ни слова о PWA и кэшировании ресурсов service worker-ами?


  1. Hrodvitnir
    08.01.2023 14:46
    +1

    Можно было бы упомянуть http/2 с server push, тоже для ускорения загрузки сделан