Пре-рендеринг: статическая генерация страниц

Пре-рендеринг (pre-rendering) в контексте Vite представляет собой процесс генерации статических HTML-страниц на этапе сборки приложения. В отличие от классического SSR, где HTML формируется на каждый запрос, или CSR, где страница собирается полностью в браузере, здесь результат фиксируется заранее и отдаётся как готовая статическая разметка.

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

Статическая генерация в Vite опирается на идею выполнения рендеринга приложения в Node.js окружении во время build-фазы. В этот момент приложение:

  • загружается как модульный граф Vite
  • исполняет маршруты заранее
  • генерирует HTML для каждого маршрута
  • сохраняет результат в dist директории

Фактически происходит «симуляция браузера» на этапе сборки, где приложение проходит те же шаги инициализации, что и при SSR, но без реального HTTP-запроса.

Ключевым результатом становится набор файлов:

dist/
  index.html
  about/index.html
  products/item-1/index.html

Отличия pre-rendering от SSR и CSR

CSR (Client-Side Rendering)

При чистом CSR:

  • сервер отдаёт пустой HTML-шаблон
  • весь UI формируется в браузере
  • данные загружаются после гидратации

Недостаток — медленный first contentful paint и слабая SEO-оптимизация.

SSR (Server-Side Rendering)

При SSR:

  • сервер генерирует HTML на каждый запрос
  • требуется Node.js сервер или edge runtime
  • сложнее инфраструктура

Проблема — высокая стоимость и сложность масштабирования.

Pre-rendering (SSG)

При pre-rendering:

  • HTML генерируется один раз на build этапе
  • сервер после деплоя не участвует в генерации
  • результат — полностью статические файлы

Это делает подход близким к JAMstack архитектуре.

Роль Vite в статической генерации

Vite сам по себе не является полноценным SSG-инструментом, но предоставляет инфраструктуру:

  • быстрый dev server с ESM модулями
  • build на основе Rollup
  • SSR API для выполнения кода в Node.js
  • плагины для трансформации графа модулей

На базе этого строятся решения:

  • vite-plugin-ssg (Vue экосистема)
  • react-snap (частично устаревший подход)
  • кастомные prerender скрипты через Vite SSR API
  • интеграции с React/Vue SSR entry

Механизм работы pre-rendering в Vite

Процесс генерации статических страниц обычно включает следующие этапы.

1. Сборка клиентского бандла

Vite выполняет стандартный build:

  • объединяет модули через Rollup
  • оптимизирует зависимости
  • генерирует ассеты (JS, CSS)

Результат используется как клиентская часть для гидратации.

2. Создание SSR entry

Для pre-rendering требуется отдельная точка входа:

// entry-server.js
import { createApp } from './app'

export async function render(url) {
  const { app, router } = createApp()

  router.push(url)
  await router.isReady()

  return {
    html: await renderToString(app)
  }
}

Эта функция позволяет отрисовать приложение в Node.js среде.

3. Генерация списка маршрутов

Pre-rendering требует заранее известного набора страниц.

Варианты получения маршрутов:

  • статический список
  • автоматический обход router конфигурации
  • файловая структура (filesystem routing)
  • crawling через entry page

Пример:

const routes = [
  '/',
  '/about',
  '/products/1'
]

4. Рендеринг каждой страницы

Каждый маршрут проходит цикл:

  • создание контекста приложения
  • установка текущего URL в router
  • ожидание асинхронных данных
  • генерация HTML через renderToString
for (const url of routes) {
  const { html } = await render(url)
  writeFile(`${outDir}${url}/index.html`, html)
}

5. Инъекция состояния

Для корректной гидратации клиент должен получить состояние, использованное при SSR:

<script>
  window.__INITIAL_STATE__ = {...}
</script>

Это позволяет избежать повторного запроса данных на клиенте.

Интеграция с Vite SSR API

Vite предоставляет нативный SSR API, который используется как основа для pre-rendering:

import { createServer } from 'vite'

const vite = await createServer({
  server: { middlewareMode: true }
})

const { render } = await vite.ssrLoadModule('/src/entry-server.js')

Этот механизм позволяет:

  • загружать серверный entry через ESM
  • использовать HMR-совместимую архитектуру
  • переиспользовать один и тот же код для dev и build

Обработка динамических маршрутов

Наиболее сложная часть pre-rendering — динамические страницы.

Статические параметры

/products/1
/products/2
/products/3

Генерируются через API или mock данные.

Динамическое определение

const products = await fetchProducts()

const routes = products.map(p => `/products/${p.id}`)

Catch-all маршруты

/products/:id
/blog/:slug

Во время pre-rendering такие маршруты разворачиваются в конкретные URL.

Работа с данными

Pre-rendering требует строгого контроля асинхронных зависимостей.

Основные стратегии:

1. Fetch на этапе render

export async function render(url) {
  const data = await fetchData(url)
  return renderToString(app(data))
}

2. Гидратация состояния

Данные сериализуются в HTML:

window.__DATA__ = {...}

3. Build-time data layer

Данные загружаются один раз при сборке:

  • CMS API
  • JSON файлы
  • GraphQL выгрузка

Интеграция с фреймворками

Vue

Часто используется vite-plugin-ssg:

  • автоматическая генерация маршрутов
  • интеграция с Vue Router
  • встроенная гидратация

React

Используется комбинация:

  • renderToString (react-dom/server)
  • Vite SSR entry
  • кастомный prerender script

Svelte

Поддерживает compile-time рендеринг:

  • генерация HTML при сборке
  • минимальный runtime

Структура выходных файлов

Типичный результат:

dist/
  assets/
  index.html
  about/
    index.html
  blog/
    post-1/
      index.html

Каждая страница уже содержит:

  • готовый HTML
  • ссылки на JS бандлы
  • критические стили

Гидратация на клиенте

После загрузки страницы происходит:

  • загрузка JS бандла
  • восстановление состояния приложения
  • привязка событий
  • синхронизация DOM
hydrateRoot(document.getElementById('app'), App)

Ключевая проблема — совпадение серверного и клиентского HTML. Несовпадение приводит к полной перерисовке.

Производительность и SEO

Pre-rendering улучшает:

  • Time To First Byte (TTFB)
  • First Contentful Paint (FCP)
  • индексирование поисковыми системами
  • доступность без JavaScript

HTML уже содержит контент, поэтому поисковые роботы получают готовую страницу.

Ограничения подхода

1. Нет runtime персонализации

Страница фиксируется на момент build.

2. Рост времени сборки

При большом количестве маршрутов build может быть медленным.

3. Данные могут устаревать

Без регенерации контент не обновляется.

4. Сложность динамических API

Все зависимости должны быть предсказуемыми.

Кеширование и оптимизация

Для ускорения сборки применяются:

  • параллельный рендер маршрутов
  • кэширование API запросов
  • инкрементальная генерация
  • разделение чанков по страницам

Комбинированные модели

На практике часто используется гибрид:

  • часть страниц pre-rendered (SEO важные)
  • часть SSR (динамические)
  • часть CSR (личные кабинеты)

Vite позволяет объединять эти модели через SSR API и плагинную систему.

Особенности архитектуры проекта

Типичная структура:

src/
  entry-client.js
  entry-server.js
  router/
  pages/
scripts/
  prerender.js
vite.config.js

Скрипт prerender выполняет основную работу генерации и вызывается после build.

Проблемы согласованности состояния

Частые ошибки:

  • различие даты/времени на сервере и клиенте
  • случайные значения (Math.random)
  • различие окружений API
  • отсутствие детерминизма

Решение — изоляция побочных эффектов и явная сериализация состояния.

Использование Vite Plugin API

Через плагины можно:

  • перехватывать HTML генерацию
  • модифицировать output bundle
  • внедрять prerender hooks
export default {
  name: 'prerender-plugin',
  closeBundle: async () => {
    await generateStaticPages()
  }
}

Это позволяет интегрировать SSG прямо в build pipeline Vite без внешних инструментов.