The post has been translated automatically. Original language: Russian
In the first part, we came to a fairly simple problem.
Server-Side Rendering allows you to get ready-made HTML from the server. But if there is a JavaScript client application behind this HTML, the browser has yet to download the code, execute it, and link the application to the existing markup.
This process is called hydration.
But what if most of the page doesn't need client-side JavaScript at all?
And even if you do, why do you have to run it all at once?
Modern frameworks answer these questions in different ways. There is no single "successor to hydration". Instead, several approaches have been formed that solve different parts of the same problem.
Solution No. 1. Not to give up hydration, but to make it less painful
Let's start with the least revolutionary option.
Hydration does not necessarily need to be thrown away.
If we have a really complex interactive application in front of us, a significant part of its interface must work on the client. Then you can optimize the process itself: reduce the amount of client JavaScript, split the code into chunks, load the component code as needed, use streaming SSR, Suspense, and prioritize hydration of different parts of the interface.
A good example of the evolution of this approach is React.
Streaming SSR: don't wait for the whole page
With traditional SSR, the server could first prepare the entire page and only then send the HTML.
Imagine a page consisting of a Header, an Article, and Comments. The data for Header and Article are already ready, while Comments depend on a slow query.
If the server waits for all work to be completed before sending a response, Comments delay receiving the rest of the page.
Streaming SSR allows you to do something else: send ready-made parts of the page to the browser without waiting for the rest to be completed.
In React, the boundaries of such expectation can be set using Suspense.:
<Header />
<Article />
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
The user can get the main content of the page while the Comments are still being prepared on the server.
But streaming primarily solves the issue of HTML delivery. By itself, it does not mean that JavaScript for interactive components has disappeared.
Selective Hydration in React: important things can be hydrated earlier
The next step is not to make different parts of the application wait for each other during hydration.
Let's say there is a search and comments on the page. The comment code is still loading, but the search is already ready for hydration. There is no need to delay its interactivity just because another part of the application is not ready yet.
React uses Suspense boundaries as independent areas that can be hydrated in stages. Moreover, React can take into account the user's interaction with an area that has not yet been hydrated and increase its priority.
This mechanism is called Selective Hydration.
It is important not to get confused in terminology here.
Selective Hydration in React and partial hydration in islands architecture are not the same thing.
Selective Hydration primarily answers the question: which part of the application should be hydrated first?
It allows you to hydrate the application in parts and set priorities. But this does not necessarily mean that some parts of the page will never be hydrated at all and will not receive client-side JavaScript.
The last question leads us to a different architecture.
Solution No.2. Partial hydration: Hydrate only what really needs it.
Consider the online magazine page.
It has an article title, several dozen paragraphs of text, a Like button, comments, and Footer.
The title of the article does not need a useState. Paragraphs don't need event handlers. Footer may never change.
Why then send the client code needed to turn all this into a part of a JavaScript application?
The idea of partial hydration is to hydrate only the interactive parts of the page.
The article can remain in plain HTML, and the Like and Comments buttons can get their own client logic.
In this case, the client application exists only where it is really needed.
Islands Architecture
One of the most well-known variants of this approach is called Islands Architecture.
The term component island appeared in 2019 from frontend architect Etsy Katie Sylor-Miller. In 2020, the idea was described in detail by the creator of Preact, Jason Miller.
The metaphor is quite accurate: the main part of the page is static HTML, and inside it there are separate interactive "islands".
For example, the text of an article can remain regular HTML, and the carousel, subscription form, and comments can work as independent interactive components with their own client JavaScript.
It is this model that has become central to Astro.
But Astro is important here as an implementation example, not as the only possible solution to the problem.
In Astro, the default UI component can be rendered in HTML without sending it to the JavaScript browser. The developer explicitly specifies which components should work on the client.
For example:
<Article />
<Comments client:visible />
The article remains server-side HTML.
For Comments, the client JavaScript will be loaded when the component approaches the viewport.
And this brings us to the next dimension of the problem.
Solution number 3. It is important not only what to hydrate, but also when
Let's assume that the component is really interactive.
This does not mean that its JavaScript is needed in the first second of loading the page.
Navigation, which the user can use immediately after opening the page, has a high priority. The subscription form at the end of a long article can only be activated when the user gets close to it. A heavy secondary widget can be postponed until the browser is free.
And a component that is not used at all in the current environment may not need to be loaded at all.
Astro, for example, provides different client directives for this.
client:load loads and hydrates the component at once.
client:idle allows you to postpone hydration until the browser's free time.
client:visible launches it when the component approaches the viewport.
client:media binds the download to the execution of the media query.
Thus, we are optimizing not only the size of the JavaScript bundle. We control the moment when specific JavaScript becomes necessary at all.
Incremental Hydration: a similar idea appears in Angular
The idea is not limited to Astro.
Modern Angular supports incremental hydration.
A part of the server page can be left in an unhydrated state and activated later according to a certain condition.
For example:
@defer (hydrate on viewport) {
<large-component />
}
In this case, the component will be hydrated when it enters the viewport.
Angular supports other conditions as well: hydration during browser downtime, after user interaction, on hover, on timer, or when a preset condition is met. It is also possible that the deferred block is not hydrated at all.
But here comes a new problem.
What happens if the user clicks on the component before we have time to hydrate it?
For such situations, Angular uses event replay: events that occurred before hydration is completed can be saved and then replayed after it.
This shows well the fundamental trade-off of deferred hydration.
The later we run JavaScript, the less work we do during the initial download. But at the same time, it is necessary to ensure correct interaction with the interface that the user already sees.
Solution No. 4. React Server Components: some of the components are not client-side at all
There is another approach that is especially important for modern React.
React Server Components is not the same as SSR.
With a regular SSR, the React component can first, render on the server, and then participate in the client application.
The Server Component remains server-based in principle. His code is not sent to the browser.
For example:
async function Article({ id }) {
const article = await db.article.find(id);
return (
<article>
<h1>{article.title}</h1>
<p>{article.text}</p>
<LikeButton />
</article>
);
}
The Article can be a Server Component.
And the interactive button:
'use client';
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(true)}>
Like
</button>
);
}
it is a Client Component.
As a result, the server part of the Article and the dependencies used only by it should not become part of the client JavaScript bundle. The border with the client runs where the interactive LikeButton begins.
Next.The js App Router builds its architecture around a combination of Server and Client Components. Upon initial download, the server result is used to quickly display HTML, and React then hydrates the Client Components.
This is not islands architecture in the strict sense, although visually the border sometimes looks similar. The mechanisms and model of the application vary.
But the engineering goal is related: not to send code to the browser that the browser does not need.
Solution No. 5. Is it possible not to hydrate at all?
So far, we have mainly discussed ways to reduce the cost of hydration.
It is possible to hydrate different parts of the application independently of each other. You can postpone hydration. Only interactive areas can be hydrated. You don't have to send the server component code to the client at all.
Qwik offers a more radical model — resumability.
The idea begins with a critique of classical hydration.
In the classical approach, the server executes the application and generates HTML. After receiving the page, the browser downloads the client JavaScript, executes it, and restores the information necessary for the application to work. After that, the application continues to work on the client.
Resumability offers a different model.
The server executes the application and serializes the information necessary to continue its operation. On the client, the application should not perform work again just to restore what was already known to the server. Instead, execution can be continued from the saved state when the appropriate logic is actually needed.
Qwik describes this difference as a transition from replay to resume: instead of repeated work on restoring the application, the client retains the ability to continue execution from the state prepared by the server.
This requires a completely different architecture of the runtime framework and imposes its own limitations.
Therefore, resumability cannot be considered just an "improved hydration setting".
This is a different answer to the same problem.
So which approach won?
Nothing.
And this is perhaps the most important conclusion.
The modern frontend has not switched from hydration to a single new technology. Instead, different ecosystems are attacking the cost of client-side JavaScript from different angles.
React develops streaming and Selective Hydration, allowing you to separate work and determine its priority.
Islands Architecture and partial hydration suggest not hydrating those parts of the page that don't need client logic at all. Astro is one notable example of this approach.
Angular with incremental hydration allows you to postpone the hydration of individual areas until it is really needed.
React Server Components draw a different boundary: certain code generally remains on the server and does not become part of the client bundle.
Qwik goes even further and offers resumability instead of the traditional restoration of an application through hydration.
In practice, these ideas are not always mutually exclusive.
What to choose
There is no universal answer, because different sites have different structures.
For an editor, CRM, email client, or a complex dashboard, a significant part of the interface is really an application. Full or Selective Hydration can be a perfectly reasonable price here.
For a blog, media, documentation, or marketing site, most of the page consists of text, images, and links. Sending JavaScript for the entire document is much more difficult to justify. Islands and partial hydration look natural here.
In a React application with a lot of server logic, Server Components allow you to draw a line between what really needs to run in the browser and what can be left on the server.
Qwik suggests abandoning the traditional restoration of an application through hydration altogether, but this is an architectural choice at the framework level, rather than a small optimization of an existing React project.
We no longer only choose between the server and the client.
The history of web rendering is sometimes described as a pendulum: at first, the interface was formed mainly on the server, then a significant part of the rendering moved to the browser, and with the spread of SSR, some of this work returned to the server.
But this is too simplistic a picture.
In fact, the question itself has changed.
Previously, we chose where to render the application — on the server or in the browser.
Now the border can be drawn at the level of individual parts of the interface.
Should a specific component be executed in the browser? Does the user need his JavaScript right away? Is it possible to hydrate it later or independently of the others? Is it possible to leave the component completely server-side? Instead of restoring the application, is it possible to continue its execution from the state prepared by the server?
Therefore, the future of server rendering is not necessarily "SSR without hydration".
Rather, it is a gradual abandonment of the assumption that the entire page should have the same lifecycle.
One part of it may remain as regular HTML. The other option is to run only on the server. The third is to be an interactive island. The fourth is to get JavaScript only after user interaction. And the user may not download some of the code at all.
Perhaps this is the main lesson of the long-term struggle with hydration.:
the cheapest JavaScript is the one that the browser didn't have to execute.
В первой части мы пришли к довольно простой проблеме.
Server-Side Rendering позволяет получить готовый HTML с сервера. Но если за этим HTML стоит клиентское JavaScript-приложение, браузеру еще предстоит скачать код, выполнить его и связать приложение с существующей разметкой.
Этот процесс называется hydration.
Но что, если большей части страницы клиентский JavaScript вообще не нужен?
И даже если нужен — почему обязательно запускать его весь и сразу?
Современные фреймворки отвечают на эти вопросы по-разному. Единого «преемника hydration» не существует. Вместо него сформировалось несколько подходов, решающих разные части одной проблемы.
Решение №1. Не отказываться от hydration, а сделать ее менее болезненной
Начнем с наименее революционного варианта.
Hydration не обязательно нужно выбрасывать.
Если перед нами действительно сложное интерактивное приложение, значительная часть его интерфейса должна работать на клиенте. Тогда можно оптимизировать сам процесс: уменьшать объем клиентского JavaScript, разбивать код на chunks, загружать код компонентов по необходимости, использовать streaming SSR, Suspense и определять приоритет гидрации разных частей интерфейса.
Хороший пример эволюции этого подхода — React.
Streaming SSR: не ждать всю страницу
При традиционном SSR сервер мог сначала подготовить всю страницу и только потом отправить HTML.
Представим страницу, состоящую из Header, Article и Comments. Данные для Header и Article уже готовы, а Comments зависят от медленного запроса.
Если сервер ждет завершения всей работы перед отправкой ответа, Comments задерживают получение остальной страницы.
Streaming SSR позволяет поступить иначе: отправить браузеру уже готовые части страницы, не дожидаясь завершения остальных.
В React границы такого ожидания можно задавать с помощью Suspense:
<Header />
<Article />
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
Пользователь может получить основное содержимое страницы, пока Comments еще готовятся на сервере.
Но streaming решает прежде всего вопрос доставки HTML. Сам по себе он не означает, что JavaScript для интерактивных компонентов исчез.
Selective Hydration в React: важное можно гидратировать раньше
Следующий шаг — не заставлять разные части приложения ждать друг друга при hydration.
Допустим, на странице есть поиск и комментарии. Код комментариев еще загружается, но поиск уже готов к гидрации. Нет необходимости откладывать его интерактивность только потому, что другая часть приложения еще не готова.
React использует Suspense boundaries как независимые области, которые могут гидратироваться поэтапно. Более того, React может учитывать взаимодействие пользователя с еще не гидратированной областью и повысить ее приоритет.
Этот механизм называют Selective Hydration.
Здесь важно не запутаться в терминологии.
Selective Hydration в React и partial hydration в islands architecture — не одно и то же.
Selective Hydration отвечает прежде всего на вопрос: какую часть приложения гидратировать раньше?
Она позволяет гидратировать приложение частями и расставлять приоритеты. Но это не обязательно означает, что некоторые части страницы вообще никогда не будут гидратированы и не получат клиентский JavaScript.
Последний вопрос приводит нас к другой архитектуре.
Решение №2. Partial hydration: гидратировать только то, чему это действительно нужно
Рассмотрим страницу интернет-журнала.
На ней есть заголовок статьи, несколько десятков абзацев текста, кнопка Like, комментарии и Footer.
Заголовку статьи не нужен useState. Абзацам не нужны обработчики событий. Footer может никогда не измениться.
Зачем тогда отправлять клиентский код, необходимый для превращения всего этого в часть JavaScript-приложения?
Идея partial hydration состоит в том, чтобы гидратировать только интерактивные части страницы.
Статья при этом может остаться обычным HTML, а кнопка Like и Comments получить собственную клиентскую логику.
Клиентское приложение в таком случае существует только там, где оно действительно требуется.
Islands Architecture
Один из наиболее известных вариантов такого подхода получил название Islands Architecture.
Термин component island появился в 2019 году у frontend-архитектора Etsy Katie Sylor-Miller. В 2020 году идею подробно описал создатель Preact Jason Miller.
Метафора довольно точная: основная часть страницы представляет собой статический HTML, а внутри нее находятся отдельные интерактивные «острова».
Например, текст статьи может оставаться обычным HTML, а карусель, форма подписки и комментарии работать как независимые интерактивные компоненты со своим клиентским JavaScript.
Именно эта модель стала центральной для Astro.
Но Astro здесь важен как пример реализации, а не как единственно возможное решение проблемы.
В Astro UI-компонент по умолчанию может быть отрендерен в HTML без отправки его JavaScript в браузер. Разработчик явно указывает, какие компоненты должны работать на клиенте.
Например:
<Article />
<Comments client:visible />
Article остается серверным HTML.
Для Comments клиентский JavaScript будет загружен, когда компонент приблизится к viewport.
И это подводит нас к следующему измерению проблемы.
Решение №3. Важно не только что гидратировать, но и когда
Предположим, компонент действительно интерактивный.
Это еще не означает, что его JavaScript нужен в первую секунду загрузки страницы.
Навигация, которой пользователь может воспользоваться сразу после открытия страницы, имеет высокий приоритет. Форму подписки в конце длинной статьи можно активировать только тогда, когда пользователь приблизится к ней. Тяжелый второстепенный виджет можно отложить до момента, когда браузер освободится.
А компонент, который вообще не используется в текущих условиях, возможно, не придется загружать вовсе.
Astro, например, предоставляет для этого разные client directives.
client:load загружает и гидратирует компонент сразу.
client:idle позволяет отложить гидрацию до свободного времени браузера.
client:visible запускает ее при приближении компонента к viewport.
client:media связывает загрузку с выполнением media query.
Таким образом, мы оптимизируем уже не только размер JavaScript bundle. Мы управляем моментом, когда конкретный JavaScript вообще становится необходим.
Incremental Hydration: похожая идея появляется и в Angular
Идея не ограничивается Astro.
Современный Angular поддерживает incremental hydration.
Часть серверной страницы можно оставить в негидратированном состоянии и активировать позже по определенному условию.
Например:
@defer (hydrate on viewport) {
<large-component />
}
В этом случае компонент будет гидратирован, когда попадет во viewport.
Angular поддерживает и другие условия: гидрацию во время простоя браузера, после взаимодействия пользователя, при наведении, по таймеру или при выполнении заданного условия. Возможен и вариант, при котором отложенный блок вообще не гидратируется.
Но здесь появляется новая проблема.
Что произойдет, если пользователь нажмет на компонент до того, как мы успели его гидратировать?
Для таких ситуаций Angular использует event replay: события, произошедшие до завершения hydration, могут быть сохранены, а затем воспроизведены после нее.
Это хорошо показывает фундаментальный компромисс отложенной hydration.
Чем позже мы запускаем JavaScript, тем меньше работы выполняем при первоначальной загрузке. Но одновременно нужно обеспечить корректное взаимодействие с интерфейсом, который пользователь уже видит.
Решение №4. React Server Components: часть компонентов вообще не является клиентской
Есть еще один подход, особенно важный для современного React.
React Server Components — не то же самое, что SSR.
При обычном SSR React-компонент может сначала отрендериться на сервере, а затем участвовать в клиентском приложении.
Server Component принципиально остается серверным. Его код не отправляется в браузер.
Например:
async function Article({ id }) {
const article = await db.article.find(id);
return (
<article>
<h1>{article.title}</h1>
<p>{article.text}</p>
<LikeButton />
</article>
);
}
Article может быть Server Component.
А интерактивная кнопка:
'use client';
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(true)}>
Like
</button>
);
}
является Client Component.
В результате серверная часть Article и используемые только ей зависимости не должны становиться частью клиентского JavaScript bundle. Граница с клиентом проходит там, где начинается интерактивный LikeButton.
Next.js App Router строит свою архитектуру вокруг сочетания Server и Client Components. При первоначальной загрузке серверный результат используется для быстрого отображения HTML, а React затем гидратирует Client Components.
Это не islands architecture в строгом смысле, хотя визуально граница иногда выглядит похоже. Механизмы и модель приложения различаются.
Но инженерная цель родственна: не отправлять в браузер код, который браузеру не нужен.
Решение №5. А можно вообще не гидратировать?
До сих пор мы в основном обсуждали способы уменьшить стоимость hydration.
Можно гидратировать разные части приложения независимо друг от друга. Можно отложить гидрацию. Можно гидратировать только интерактивные области. Можно вообще не отправлять клиенту код серверных компонентов.
Qwik предлагает более радикальную модель — resumability.
Идея начинается с критики классической hydration.
При классическом подходе сервер выполняет приложение и формирует HTML. После получения страницы браузер загружает клиентский JavaScript, выполняет его и восстанавливает информацию, необходимую для работы приложения. После этого приложение продолжает работу уже на клиенте.
Resumability предлагает другую модель.
Сервер выполняет приложение и сериализует информацию, необходимую для продолжения его работы. На клиенте приложение не должно заново выполнять работу только ради восстановления того, что уже было известно серверу. Вместо этого выполнение можно продолжить с сохраненного состояния тогда, когда соответствующая логика действительно понадобится.
Qwik описывает эту разницу как переход от replay к resume: вместо повторной работы по восстановлению приложения на клиенте сохраняется возможность продолжить выполнение с состояния, подготовленного сервером.
Это требует совершенно другой архитектуры framework runtime и накладывает собственные ограничения.
Поэтому resumability нельзя считать просто «улучшенной настройкой hydration».
Это другой ответ на ту же проблему.
Так какой подход победил?
Никакой.
И это, пожалуй, самый важный вывод.
Современный frontend не перешел с hydration на единственную новую технологию. Вместо этого разные экосистемы атакуют стоимость клиентского JavaScript с разных сторон.
React развивает streaming и Selective Hydration, позволяя разделять работу и определять ее приоритет.
Islands Architecture и partial hydration предлагают вообще не гидратировать те части страницы, которым не нужна клиентская логика. Astro — один из заметных примеров такого подхода.
Angular с incremental hydration позволяет откладывать гидрацию отдельных областей до момента, когда она действительно понадобится.
React Server Components проводят другую границу: определенный код вообще остается на сервере и не становится частью клиентского bundle.
Qwik идет еще дальше и предлагает resumability вместо традиционного восстановления приложения посредством hydration.
На практике эти идеи не всегда являются взаимоисключающими.
Что выбрать
Универсального ответа нет, потому что разные сайты имеют разную структуру.
Для редактора, CRM, почтового клиента или сложного dashboard значительная часть интерфейса действительно является приложением. Полная или Selective Hydration здесь может быть совершенно разумной ценой.
Для блога, медиа, документации или маркетингового сайта большая часть страницы состоит из текста, изображений и ссылок. Отправлять JavaScript для всего документа гораздо сложнее оправдать. Здесь islands и partial hydration выглядят естественно.
В React-приложении с большим количеством серверной логики Server Components позволяют провести границу между тем, что действительно должно выполняться в браузере, и тем, что можно оставить на сервере.
Qwik предлагает вообще отказаться от традиционного восстановления приложения посредством hydration, но это уже архитектурный выбор на уровне фреймворка, а не небольшая оптимизация существующего React-проекта.
Мы больше не выбираем только между сервером и клиентом
Историю веб-рендеринга иногда описывают как маятник: сначала интерфейс формировался преимущественно на сервере, затем значительная часть рендеринга переехала в браузер, а с распространением SSR часть этой работы снова вернулась на сервер.
Но это слишком упрощенная картина.
На самом деле изменился сам вопрос.
Раньше мы выбирали, где рендерить приложение — на сервере или в браузере.
Теперь границу можно проводить на уровне отдельных частей интерфейса.
Должен ли конкретный компонент выполняться в браузере? Нужен ли его JavaScript пользователю сразу? Можно ли гидратировать его позже или независимо от остальных? Можно ли оставить компонент полностью серверным? А можно ли вместо восстановления приложения продолжить его выполнение с состояния, подготовленного сервером?
Поэтому будущее server rendering — это не обязательно «SSR без hydration».
Скорее это постепенный отказ от предположения, что вся страница должна иметь одинаковый жизненный цикл.
Одна ее часть может остаться обычным HTML. Другая — выполняться только на сервере. Третья — быть интерактивным island. Четвертая — получить JavaScript только после взаимодействия пользователя. А некоторый код пользователь может не скачать вообще.
Возможно, именно это и есть главный урок многолетней борьбы с hydration:
самый дешевый JavaScript — тот, который браузеру не пришлось выполнять.