Разделение клиентской и серверной сборки
В архитектуре SSR (Server-Side Rendering) с использованием Vite ключевым моментом становится разделение итогового результата сборки на два независимых артефакта: клиентский бандл и серверный бандл. Эти два набора файлов решают разные задачи и имеют различные требования к окружению выполнения.
Клиентский бандл предназначен для браузера. Он включает в себя JavaScript, стили, статические ассеты и код гидратации, который «оживляет» уже отрендеренный на сервере HTML. Серверный бандл исполняется в Node.js или другом серверном окружении и отвечает за генерацию HTML на лету, обработку маршрутов и подготовку состояния приложения.
В Vite разделение реализуется на уровне конфигурации сборки и Rollup-пайплайна, поскольку финальная сборка в production-режиме использует Rollup как основной бандлер.
Конфигурация клиентской сборки
Клиентская сборка формируется стандартной командой:
vite build
или явно:
vite build --ssrManifest
Основной результат помещается в директорию dist/client.
Типичная конфигурация:
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: 'dist/client',
manifest: true,
rollupOptions: {
input: '/src/entry-client.js'
}
}
})
Ключевым элементом является entry point клиента — файл, который выполняет гидратацию:
import { createApp } from './app'
createApp().mount('
В результате клиентская сборка содержит:
Manifest используется сервером для определения, какие файлы нужно подключить в HTML.
Формирование серверного бандла
Серверная сборка запускается отдельной командой:
vite build --ssr src/entry-server.js
или через конфигурацию:
export default defineConfig({
build: {
ssr: 'src/entry-server.js',
outDir: 'dist/server',
target: 'node18',
rollupOptions: {
output: {
format: 'esm'
}
}
}
})
Серверный entry обычно экспортирует функцию рендера:
import { createApp } from './app'
export async function render(url) {
const app = createApp()
return {
html: await app.renderToString()
}
}
Серверный бандл отличается следующими характеристиками:
Externalизация зависимостей
При SSR важно не тащить все зависимости в серверный бандл. Vite автоматически внешизирует Node-модули:
export default defineConfig({
ssr: {
external: ['express']
}
})
или отключает внешизацию:
ssr: {
noExternal: ['some-esm-package']
}
Это позволяет гибко управлять тем, какие пакеты должны попасть в бандл, а какие будут импортироваться через Node.js напрямую.
Различия в обработке модулей
Клиентская сборка ориентирована на ESM в браузере и оптимизацию загрузки:
Серверная сборка ориентирована на минимизацию накладных расходов:
Код-сплиттинг и SSR
Vite использует Rollup для разбиения кода на чанки:
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
}
}
})
На клиенте это улучшает загрузку страниц, позволяя браузеру кэшировать стабильные зависимости.
На сервере код-сплиттинг работает иначе: слишком мелкие чанки могут
ухудшить производительность из-за увеличения количества
require/import операций.
SSR Manifest и привязка ресурсов
При включённой опции:
build: {
manifest: true
}
Vite генерирует manifest.json:
{
"src/entry-client.js": {
"file": "assets/entry-client.8d3f2.js",
"css": ["assets/entry-client.9a1c.css"]
}
}
Этот файл используется сервером для вставки правильных тегов:
const manifest = JSON.parse(
fs.readFileSync('dist/client/.vite/manifest.json', 'utf-8')
)
const entry = manifest['src/entry-client.js']
const scripts = `<script type="module" src="/${entry.file}"></script>`
Синхронизация клиентского и серверного окружения
Одной из ключевых проблем SSR является необходимость идентичного состояния на сервере и клиенте. Серверный бандл формирует HTML, а клиентский должен продолжить выполнение без расхождений.
Для этого:
Пример безопасного модуля:
export function createStore() {
return {
state: {}
}
}
Небезопасный вариант:
// плохо для SSR
window.state = {}
Обработка CSS и ассетов
Vite извлекает CSS из модулей и подключает его отдельно в клиентской сборке:
Серверный бандл не содержит CSS как исполняемый код, но использует его через manifest.
Динамические импорты и SSR
Dynamic import играет ключевую роль:
const module = await import('./heavy-module.js')
На клиенте это приводит к созданию отдельного чанка. На сервере это просто асинхронная загрузка модуля в Node.js.
Важно учитывать:
Оптимизация серверного бандла
Типичные настройки:
export default defineConfig({
build: {
ssr: true,
minify: false,
sourcemap: true,
target: 'node18'
}
})
Отключение minify на сервере часто оправдано:
Изоляция окружений
Ключевая задача SSR сборки — строгая изоляция:
Типичный паттерн:
export const isServer = typeof window === 'undefined'
Результирующая структура сборки
После выполнения сборки формируется структура:
dist/
client/
assets/
manifest.json
index.html (опционально)
server/
entry-server.js
chunks/
Эта структура позволяет серверу: