Создание SSR-точки входа

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

В контексте Vite SSR разделение ответственности становится строгим: клиентская точка входа отвечает за гидратацию и дальнейшую интерактивность, серверная — за первичную генерацию HTML и подготовку состояния приложения.

Базовая структура SSR-приложения в Vite

Типичная SSR-архитектура в Vite включает несколько ключевых файлов:

  • серверная точка входа (server entry)
  • клиентская точка входа (client entry)
  • HTML-шаблон
  • серверный runtime для выполнения модулей Vite

Серверная точка входа формирует HTML-строку и возвращает её в HTTP-ответе, а клиентская подхватывает уже готовую разметку.

Разделение особенно важно, поскольку код, выполняемый на сервере, не имеет доступа к браузерным API (window, document, localStorage), а клиентский код не должен содержать серверной логики.

Серверная точка входа: назначение и структура

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

Минимальная структура:

export function render(url, manifest) {
  return {
    html: '',
    state: {}
  }
}

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

Ключевая особенность SSR в Vite заключается в том, что серверная точка входа загружается через специальный API:

vite.ssrLoadModule('/src/entry-server.js')

Это позволяет использовать ES-модули напрямую без предварительной сборки.

Использование vite.ssrLoadModule

Механизм ssrLoadModule является центральным элементом SSR в Vite. Он выполняет загрузку модулей в Node.js с учётом трансформаций Vite, включая:

  • поддержку ESM-импорта
  • трансформацию TypeScript
  • обработку алиасов
  • поддержку HMR в dev-режиме

Серверная точка входа должна быть совместима с этим механизмом, то есть не должна использовать Node-специфичные require-конструкции, если проект ориентирован на ESM.

Пример интеграции:

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

После загрузки функция render используется для генерации HTML.

Создание server-entry модуля

Серверный entry-модуль обычно создаёт экземпляр приложения без привязки к DOM:

import { createApp } from './app'

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

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

  const html = await renderToString(app)

  return {
    html
  }
}

В этом коде важен порядок:

  1. создаётся приложение
  2. устанавливается маршрут
  3. дожидается завершения асинхронных переходов
  4. выполняется рендер в строку

Функция renderToString может быть из @vue/server-renderer, React DOM Server или аналогичного SSR-движка.

Клиентская точка входа и связь с SSR

Клиентская точка входа выполняет гидратацию HTML, сгенерированного сервером.

import { createApp } from './app'

createApp().mount('#app')

Важно, что клиентская часть не выполняет повторный рендер, а только активирует уже существующую DOM-структуру.

SSR-архитектура требует синхронизации состояния между сервером и клиентом. Обычно используется передача состояния через глобальный объект:

window.__INITIAL_STATE__

Сервер записывает состояние, клиент его считывает.

Интеграция SSR с HTML-шаблоном Vite

HTML-шаблон Vite играет роль контейнера для SSR-результата. Обычно он содержит:

<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>

Сервер заменяет комментарий на сгенерированный HTML:

template.replace('<!--ssr-outlet-->', html)

Также может добавляться сериализованное состояние:

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

SSR в режиме разработки (middleware mode)

Vite предоставляет middleware-режим для SSR, позволяющий интегрировать dev-сервер с Node.js сервером (например, Express или Koa).

import express from 'express'
import { createServer as createViteServer } from 'vite'

const app = express()

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

app.use(vite.middlewares)

В этом режиме SSR-точка входа загружается динамически при каждом запросе, что обеспечивает мгновенное отражение изменений кода.

Обработка маршрутизации в SSR entry

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

Типичный поток:

  • получение URL из HTTP-запроса
  • установка маршрута в router
  • ожидание готовности данных
  • генерация HTML

Критически важно дождаться завершения всех асинхронных операций перед рендерингом.

Ошибка в этом месте приводит к частично отрендеренному HTML и рассинхронизации гидратации.

Асинхронные данные в SSR entry

SSR требует предварительной загрузки данных до генерации HTML. Это решается через специальные хуки:

await Promise.all(routeComponents.map(loadData))

Каждый компонент может иметь метод загрузки:

Component.asyncData = async (store, route) => {
  await store.fetch(route.params.id)
}

Серверная точка входа агрегирует эти вызовы.

Сборка SSR через Vite build

Vite поддерживает отдельную сборку для серверной части:

vite build --ssr src/entry-server.js

Результат представляет собой Node.js-совместимый модуль, оптимизированный для выполнения на сервере.

Особенности SSR-сборки:

  • удаление клиентских API
  • сохранение ESM-структуры
  • оптимизация импортов
  • генерация manifest-файла

Использование manifest для SSR

Manifest используется для связывания клиентских ассетов с серверным рендерингом.

Он содержит:

  • список JS-чанков
  • CSS зависимости
  • динамические импорты

Серверная точка входа использует manifest для вставки правильных <script> и <link> тегов:

const scripts = manifest['src/entry-client.js'].file

Это обеспечивает корректную загрузку клиентского бандла.

Ошибки проектирования SSR entry

Распространённые проблемы:

  • использование глобального состояния между запросами
  • отсутствие изоляции контекста приложения
  • утечка памяти из-за кеширования router/app instance
  • выполнение браузерного кода на сервере
  • отсутствие ожидания асинхронных данных

Каждый запрос должен создавать новый экземпляр приложения.

Изоляция контекста запроса

SSR требует строгой изоляции:

export function createApp() {
  const app = new App()
  const router = new Router()

  return { app, router }
}

Каждый вызов SSR entry должен формировать новый контекст, исключая shared state.

Поток SSR рендеринга в Vite

Полный процесс включает последовательные шаги:

  1. HTTP запрос
  2. загрузка SSR entry через ssrLoadModule
  3. создание приложения
  4. установка маршрута
  5. загрузка данных
  6. рендер в HTML
  7. вставка в template
  8. отправка ответа

Каждый этап критичен для корректной работы SSR.

Потенциальные расширения SSR entry

SSR entry может быть расширена дополнительными слоями:

  • кэширование HTML на уровне сервера
  • streaming SSR (постепенная отдача HTML)
  • интеграция с edge runtime
  • prefetch данных через GraphQL или REST
  • разделение по регионам (multi-region rendering)

Streaming SSR особенно важен для крупных приложений, где полный рендер занимает значительное время.

Streaming SSR и Vite

При использовании потокового рендеринга HTML формируется по частям:

const stream = renderToNodeStream(app)
stream.pipe(res)

SSR entry в этом случае возвращает не строку, а поток, что снижает время до первого байта.

Роль SSR entry в масштабируемых приложениях

SSR-точка входа становится центральным элементом серверной архитектуры. От её структуры зависит:

  • стабильность гидратации
  • производительность первого рендера
  • корректность маршрутизации
  • поведение при параллельных запросах

В Vite SSR entry не является просто файлом, а полноценным слоем исполнения приложения в серверной среде, связывающим систему модулей Vite и runtime Node.js.