Главная » Статьи » Серверный рендеринг в JavaScript: оптимизация производительности

Серверный рендеринг в JavaScript: оптимизация производительности

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

Получать при рендеринге

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

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

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

Серверный рендеринг в JavaScript: оптимизация производительности

Однако затем мы добавили разделение кода, и теперь эти запросы не запускаются до тех пор, пока не загрузится код этого модуля. В серверном рендеринге в JavaScript: почему SSR? я объяснил, как предварительная загрузка ресурсов на странице может удалить этот каскад, но это не поможет нам при следующей навигации. Ну, это тоже предзагрузка … Может быть.

Однако альтернатива есть. Отделите загрузку данных от компонента представления. Сделайте так, чтобы этот компонент-оболочка запускал загрузку данных и загружал компонент представления через lazy загрузку и отображал его по мере его необходимости. React Suspense — отличный пример того, как с этим справиться, но есть много способов добиться подобного результата.

// ProfilePage.js
const ProfileDetails = lazy(() => import("./ProfileDetails.js")); function ProfilePage() { // This is not a Promise. It's a special object // from a Suspense integration. const resource = fetchProfileData(); return ( <Suspense fallback={<h1>Loading profile...</h1>}> <ProfileDetails user={resource.user} /> </Suspense> );
} // ProfileDetails.js
function ProfileDetails(props) { // Try to read user info, although it might not have loaded yet const user = props.user.read(); return <h1>{user.name}</h1>;
}

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

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

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

На ум приходят фрагменты GraphQL. Хотя это не единственное решение, оно предъявляет большие требования к клиентской службе API. Facebook’s Relay является ярким примером попытки упростить эту задачу для конечного пользователя. React проявил достаточно беспокойства, чтобы подумать о том, чтобы предложить решение без API с компонентами React Server.

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

Потоковая передача

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

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

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

Серверный рендеринг в JavaScript: оптимизация производительности

Как это устроено

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

Начнём с рендеринга синхронного контента и рендеринга плейсхолдеров с асинхронными границами. Во многих библиотеках уже есть методы для этого с помощью тегов Suspense или Await. Затем, когда данные возвращаются из асинхронного запроса, вы визуализируете контент на сервере и отправляете его на страницу после предыдущего контента в div с значением display: none. Затем мы пишем тег script, чтобы вставить новые узлы туда, где находится плейсхолдер, и загрузить сериализованные данные для гидратации. Когда все асинхронные данные завершены, мы отправляем конец страницы и закрываем поток.

В этой статье 2014 года автора Marko гораздо более подробно рассказывается о том, как это работает. В сочетании с частичной гидратацией страница может сразу стать интерактивной, не дожидаясь загрузки дополнительного кода JavaScript. Помимо преимуществ в производительности, он все еще работает с SEO, когда на странице не выполняется JavaScript (контент не упорядочен).

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

Так насколько же это может быть эффективным? Я использовал Solid для рендеринга одного и того же простого приложения несколькими разными методами. Сравните, как выглядит ожидание ресурсов в обычных фреймворках, таких как Nextjs, Nuxt, SvelteKit:

Серверный рендеринг в JavaScript: оптимизация производительности

Большое изображение

К той же загрузке страницы с потоковой передачей:

Серверный рендеринг в JavaScript: оптимизация производительности

Большое изображение

Мало того, что первые цвета появляются намного быстрее, набирая 180 мс вместо 450 мс. Общий профиль загрузки сжимается, потому что JavaScript, используемый для гидратации, уже загружен. Пример потоковой передачи в основном выполняется за 260 мс, тогда как тот, в котором мы ждем, занимает до 500 мс для завершения своего выполнения.

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

Серверный рендеринг в JavaScript: оптимизация производительности

Большое изображение

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

Но, нет… Эти тесты пока проводились в быстрых сетях. В более медленных сетях будет другая история для клиента. Разница между серверными методами становится пропорционально менее важной, но клиент остается в тени, как мы можем видеть, сравнивая потоковую передачу на «Fast 3G»:

Серверный рендеринг в JavaScript: оптимизация производительности

Большое изображение

Для нашей клиентской версии:

Серверный рендеринг в JavaScript: оптимизация производительности

Большое изображение

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

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

Заключение

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

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

Автор: Ryan Carniato

Источник: dev.to

Редакция: Команда webformyself.