Рендеринг веб-страниц¶
Рендеринг - сборка HTML страницы из шаблона или компонентов и данных. Его делают на сервере на каждый запрос, заранее при сборке или в браузере из JS-бандла. От этого зависит, что пользователь и поисковик увидят в первом ответе.
SSR (server-side rendering) - HTML первого ответа собран на сервере до того, как браузер выполнит клиентский JS. Кроме шаблонизаторов вроде Thymeleaf и Jinja и фреймворков поверх React, Vue и Angular есть и другие способы: компоненты рендерят через API самих UI-фреймворков, страницы собирают заранее (SSG), режимы смешивают в одном приложении, HTML отдают потоком. Статические страницы - отдельный случай (см. Что не является SSR).
Note
В узком смысле SSR - рендер на каждый запрос (runtime SSR), а HTML, собранный при сборке, называют SSG. Слово
«prerendering» в доках значит разное. В Angular это сборка HTML при ng build, то есть SSG. В Next.js (Pages
Router) это общее название SSG и SSR: оба готовят HTML до браузера. В App Router вместо него говорят static и dynamic
rendering.
CSR и hydration¶
CSR (client-side rendering) - сервер отдаёт почти пустой HTML и JS-бандл, страницу собирает браузер.
<body>
<div id="root"></div> <!-- сюда React нарисует приложение -->
<script type="module" src="/assets/index-B7f3a.js"></script>
</body>
- Так работает классическое SPA: React или Vue на Vite, Angular без SSR.
- Сервер раздаёт статику и JSON API. Данные страница запрашивает отдельно, уже после загрузки.
- Пока бандл не загрузился и не выполнился, пользователь видит пустую страницу или спиннер.
- Поисковики и превью ссылок в мессенджерах получают пустой
<div id="root">. Google выполняет JS, но позже и не всегда. Большинство ботов превью JS не выполняют.
Диагностика: кто рендерил страницу, видно по HTML первого ответа.
Пустой <div id="root"></div> - CSR, текст страницы в разметке - SSR или SSG. В браузере HTML ответа показывает View
Source (Cmd+Option+U), а вкладка Elements в DevTools - DOM после работы JS. Второй способ: выключить JavaScript
(DevTools -> Cmd+Shift+P -> Disable JavaScript). SSR-страница видна, но кнопки не работают, CSR-страница пустая.
Hydration (гидратация) - клиентский JS «оживляет» HTML, пришедший с сервера: строит то же дерево компонентов, сопоставляет его с готовым DOM и вешает обработчики событий, не пересоздавая разметку.
- Нужна, когда HTML отрендерил компонентный фреймворк (SSR, SSG), а страница должна стать интерактивной.
- Пока JS не загрузился и не выполнился, страница видна, но клики не работают.
- Клиент всё равно грузит JS всех компонентов страницы. SSR ускоряет первый показ, но не уменьшает бандл.
- Если клиент отрендерил не то, что сервер (текущее время,
Math.random(),windowв рендере), это hydration mismatch. React пишет ошибку в консоль и перерисовывает расхождение на клиенте.
Итого: SSR даёт быстрый первый показ, hydration - интерактивность. Между ними страница видна, но не реагирует.
Серверные шаблоны¶
Сервер берёт шаблон с плейсхолдерами, подставляет данные и отдаёт готовый HTML. Рендер на каждый запрос, JS-фреймворк на клиенте не нужен. Самый старый вид SSR: так работали CGI-скрипты, PHP и JSP задолго до SPA.
| Шаблонизатор | Язык и фреймворк |
|---|---|
| Thymeleaf | Java, Spring MVC |
| FreeMarker | Java |
| JSP | Java, Jakarta EE. Legacy |
| Jinja | Python, Flask |
| Django templates | Python, Django |
html/template |
Go, стандартная библиотека |
| ERB | Ruby, Rails |
| Twig | PHP, Symfony |
| Razor | C#, ASP.NET Core |
<!-- users/list.html: Thymeleaf подставит users из Model -->
<ul>
<li th:each="user : ${users}" th:text="${user.name}">Имя</li>
</ul>
В Spring MVC это @Controller, который кладёт данные в Model и возвращает имя шаблона
(см. View и ViewResolver).
- Hydration нет: на клиенте нет копии шаблона, перерисовать кусок страницы без запроса к серверу нельзя.
- Интерактивность - отдельный JS поверх готового HTML или HTML-over-the-wire: HTMX, Hotwire Turbo. Сервер отвечает на AJAX-запрос куском HTML, библиотека вставляет его в страницу.
Когда брать: админки, CRUD, внутренние системы, проекты, где бэкенд-команда работает без фронтендеров.
Рендер компонентов на сервере¶
SSR через API фреймворка - те же компоненты React, Vue или Angular, что работают в браузере, рендерятся в HTML-строку на сервере, обычно в Node.js. Next.js и Nuxt для этого не обязательны: серверный API есть у самих фреймворков.
| Фреймворк | Сервер: компоненты -> HTML | Клиент: hydration |
|---|---|---|
| React | renderToString, renderToPipeableStream из react-dom/server |
hydrateRoot из react-dom/client |
| Vue 3 | renderToString из vue/server-renderer |
createSSRApp(App).mount('#app') |
| Angular | Пакет @angular/ssr (до v17 - Angular Universal), ставится ng add @angular/ssr |
provideClientHydration() |
// server.js: Express отдаёт HTML, собранный из компонентов
import {renderToString} from 'react-dom/server';
app.get('*', (req, res) => {
const html = renderToString(<App url={req.url}/>);
res.send(`<div id="root">${html}</div><script src="/client.js"></script>`);
});
// client.js: тот же <App /> оживляет готовый HTML
import {hydrateRoot} from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App url={location.pathname}/>);
react-dom- рендерер React для браузера, в нём же лежат hydration и серверный API.- Роутинг, загрузку данных до рендера, передачу данных на клиент и сборку двух бандлов (серверного и клиентского) пишешь сам.
Когда брать: если SSR встраивают в свой Node-сервер или нужен полный контроль. В остальных случаях берут мета-фреймворк.
Мета-фреймворк - фреймворк поверх UI-фреймворка. Из коробки даёт роутинг по файлам, загрузку данных на сервере, SSR, SSG и hydration. Режим рендеринга выбирается для каждой страницы.
| UI-фреймворк | Мета-фреймворк |
|---|---|
| React | Next.js, React Router v7 (бывший Remix) |
| Vue | Nuxt |
| Svelte | SvelteKit |
| Solid | SolidStart |
| Angular | Analog |
Note
Angular SSR - не отдельный мета-фреймворк, а официальный пакет @angular/ssr в Angular CLI. Он закрывает то же,
что Next.js для React: SSR, prerendering, hybrid rendering.
Путь запроса:
- Запрос приходит на Node-сервер фреймворка.
- Сервер загружает данные страницы:
getServerSidePropsв Next.js Pages Router, async-компонент в App Router,useFetchв Nuxt. - Рендерит компоненты в HTML и встраивает данные в страницу JSON-ом в
<script>, чтобы клиент не запрашивал их повторно. - Браузер показывает HTML, грузит JS и делает hydration.
- Дальше переходы как в SPA, без полной перезагрузки. За следующей страницей клиент ходит к серверу за данными, а не за целым HTML.
Когда брать: публичные страницы, где важны SEO и быстрый первый показ, а интерактивности много: магазины, медиа, маркетплейсы.
Рендер при сборке¶
SSG (static site generation) - HTML всех страниц собирается заранее, при сборке, и лежит готовыми файлами. Его отдаёт nginx или CDN как статику, рендера на запрос нет. Angular называет это prerendering.
- Пользователь сразу получает HTML, как при SSR. Разница в том, что рендер один раз, а не на каждый запрос.
- Данные свежие на момент сборки. Изменились - пересобирай.
- Компонентные SSG (Next.js, Nuxt) после загрузки делают hydration. Чистые генераторы (Hugo, MkDocs) отдают просто HTML.
Примеры: MkDocs (этот конспект: properdocs build складывает HTML в site/), Hugo, Jekyll, Eleventy,
Docusaurus, VitePress, Astro, Next.js (output: 'export'), Nuxt (nuxt generate), Angular (prerender при
ng build).
Когда брать: документация, блоги, лендинги. Контент меняется редко и одинаков для всех.
ISR (Incremental Static Regeneration) - SSG с пересборкой отдельных страниц на работающем сервере. Страница отдаётся из кеша и пересобирается в фоне раз в N секунд или по событию. Пока идёт пересборка, отдаётся старая версия (stale-while-revalidate).
- По событию:
revalidatePath('/blog')после правки в CMS. - Термин из Next.js. В Nuxt похожее задаётся в
routeRulesопциямиisrиswr.
Когда брать: страниц много или контент меняется чаще, чем удобно пересобирать весь сайт: каталог товаров, новости.
Streaming SSR¶
Сервер отправляет HTML кусками по мере готовности, а не целиком в конце: сначала каркас страницы, потом блоки, которые
ждали медленные данные. В React 18+ это renderToPipeableStream (Node.js) и renderToReadableStream (Web Streams),
в Next.js App Router - loading.js и <Suspense>.
- Сервер сразу отдаёт shell: всё, что вне
<Suspense>, а на месте медленных блоков -fallback, например спиннер. - Соединение остаётся открытым. Когда данные блока готовы, сервер дописывает в тот же ответ его HTML и маленький
<script>, который ставит этот HTML на место fallback. - Hydration тоже идёт по частям (selective hydration): блок оживает, как только пришли его HTML и JS. Блок, по которому кликнули, гидрируется первым.
<Layout>
<Product/> {/* в shell, приходит сразу */}
<Suspense fallback={<Spinner/>}>
<Reviews/> {/* медленный запрос, придёт следующим куском */}
</Suspense>
</Layout>
- Транспорт - обычный HTTP:
Transfer-Encoding: chunkedв HTTP/1.1, поток DATA-фреймов в HTTP/2. - Статус и заголовки уходят с первым куском. Если блок упал после shell, поменять 200 на 500 или сделать redirect уже нельзя, ошибку показывают внутри страницы.
- Идея старая:
flush()в PHP, BigPipe у Facebook (2010). Новое - поддержка в React и мета-фреймворках из коробки.
Диагностика:
Когда брать: страница зависит от медленных источников данных, а каркас нужно показать сразу.
Гибридный рендеринг¶
Разные части приложения рендерятся разными способами: часть статическая, часть рендерится на сервере на запрос, часть оживает только на клиенте. Смешивают на трёх уровнях: по маршрутам, внутри страницы и по компонентам.
По маршрутам - каждая страница выбирает свой режим. Angular называет это hybrid rendering:
// app.routes.server.ts, Angular 19+
export const serverRoutes: ServerRoute[] = [
{path: '', renderMode: RenderMode.Prerender}, // при сборке, SSG
{path: 'account', renderMode: RenderMode.Client}, // только в браузере, CSR
{path: '**', renderMode: RenderMode.Server}, // остальное на каждый запрос, SSR
];
В Next.js и Nuxt режим тоже задаётся на уровне страницы, в Nuxt - через routeRules.
Внутри страницы: Partial Prerendering (PPR) - режим Next.js. Статический каркас страницы собран при сборке и
отдаётся сразу, динамические части в <Suspense> рендерятся на запрос и стримятся в тот же ответ. Это SSG и
streaming SSR на одной странице.
По компонентам: Islands (архитектура островов) - страница из статического HTML без JS с отдельными интерактивными «островами»: поиск, корзина, карусель. JS грузится и hydration идёт только для островов, каждый независимо (partial hydration).
<!-- Astro -->
<Header/> <!-- только HTML, JS на клиент не уходит -->
<Search client:visible/> <!-- остров: hydration, когда попал в экран -->
Примеры: Astro, Fresh (Deno), Eleventy с <is-land>.
Когда брать: контентные сайты с небольшой интерактивностью: блоги, документация, витрины магазинов.
По компонентам: React Server Components (RSC) - компоненты React, которые выполняются только на сервере или при
сборке. Их код не попадает в бандл клиента и не гидрируется. Интерактивные компоненты помечаются 'use client': их
рендерят на сервере в HTML и гидрируют в браузере, как при обычном SSR.
// ProductPage.jsx - серверный компонент: async, читает базу напрямую, в браузер не уходит
import {AddToCart} from './AddToCart';
export default async function ProductPage({id}) {
const product = await db.product.find(id);
return <AddToCart id={id} title={product.name}/>;
}
// AddToCart.jsx - клиентский компонент: гидрируется, можно useState и onClick
'use client';
export function AddToCart({id, title}) { /* ... */
}
- Результат серверных компонентов - не HTML, а RSC payload: сериализованное дерево, которое React на клиенте встраивает в страницу.
- RSC и SSR - разные механизмы и работают вместе. RSC решает, чей код едет в браузер, SSR - как получить HTML первого ответа.
- В Next.js App Router компоненты серверные по умолчанию.
Когда брать: Next.js App Router, когда нужно меньше JS на клиенте и загрузка данных прямо в компоненте.
Что не является SSR¶
- Рукописный статический HTML.
index.html, свёрстанный руками, - не SSR и не SSG. Сервер ничего не генерирует, просто отдаёт файл. - HTML, сгенерированный заранее. Это SSG. Для пользователя эффект как у SSR, но рендер один раз при сборке, а не на запрос.
- CSR. HTML собирает браузер. Даже если сервер отдаёт
index.htmlдинамически, контента в нём нет.
Сравнение¶
| Способ | Где собирается HTML | Когда | Hydration | Первый показ и SEO | Примеры |
|---|---|---|---|---|---|
| CSR | Браузер | После загрузки JS | Не нужна, весь рендер на клиенте | Пустая страница до JS, SEO плохое | React или Vue на Vite, Angular без SSR |
| Серверные шаблоны | Сервер | На каждый запрос | Нет, свой JS поверх HTML | Сразу | Thymeleaf, Jinja, Razor |
| SSR компонентов | Сервер (Node.js) | На каждый запрос | Вся страница | Сразу, клики после hydration | Next.js, Nuxt, Angular SSR |
| SSG | Сборщик | Один раз при сборке | Вся страница у Next.js и Nuxt, нет у Hugo и MkDocs | Сразу, файл с CDN | MkDocs, Hugo, Astro |
| ISR | Сервер в фоне | При сборке, потом по таймеру или событию | Вся страница | Как у SSG | Next.js, Nuxt |
| Streaming SSR | Сервер | На запрос, кусками | По частям | Каркас сразу, медленные блоки позже | React 18+, Next.js App Router |
| Islands | Сборщик или сервер | При сборке или на запрос | Только острова | Сразу, минимум JS | Astro, Fresh |
| RSC | Сервер или сборщик | На запрос или при сборке | Только компоненты 'use client' |
Сразу, меньше JS | Next.js App Router |
Итого: больших лагеря два -- серверные шаблоны и серверный рендер компонентных приложений (React, Vue, Angular на сервере). Остальное - вариации по осям:
- когда рендерить: на каждый запрос, при сборке, в фоне по таймеру
- сколько рендерить на сервере: всю страницу, каркас, отдельные компоненты
- гидрировать ли клиент: всю страницу, только острова, ничего
- отдавать HTML целиком или кусками