Перейти к содержимому

12. SSR с Vite

Vite предоставляет первоклассную поддержку Server-Side Rendering. В отличие от client-side только приложений, SSR рендерит HTML на сервере и отправляет уже готовую разметку браузеру.

SSR запрос:
1. Browser → GET /dashboard
2. Express/Fastify → handler
3. vite.ssrLoadModule('./src/entry-server.tsx') → рендер
4. React.renderToString(<App />) → HTML строка
5. Inject в index.html шаблон
6. Response → полный HTML
Hydration:
7. Browser получает HTML → отображает сразу
8. Browser загружает JS бандл
9. React "оживляет" (hydrates) существующий DOM
my-ssr-app/
├── index.html ← Шаблон с <!--app-html--> placeholder
├── src/
│ ├── main.tsx ← Client entry (для hydration)
│ ├── entry-server.tsx ← Server entry
│ └── App.tsx
├── server.js ← Express/Fastify сервер
└── vite.config.ts
src/entry-server.tsx
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom/server'
import { App } from './App'
export function render(url: string) {
const html = renderToString(
<StaticRouter location={url}>
<App />
</StaticRouter>
)
return { html }
}
src/main.tsx
import { hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { App } from './App'
hydrateRoot(
document.getElementById('root')!,
<BrowserRouter>
<App />
</BrowserRouter>
)
server.js
import express from 'express'
import { createServer as createViteServer } from 'vite'
async function createServer() {
const app = express()
// Dev режим: используем Vite middleware
if (process.env.NODE_ENV !== 'production') {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
})
app.use(vite.middlewares)
app.use('*', async (req, res, next) => {
try {
const url = req.originalUrl
// Читаем index.html шаблон
let template = fs.readFileSync('index.html', 'utf-8')
// Vite трансформирует шаблон (инжектирует HMR)
template = await vite.transformIndexHtml(url, template)
// Загружаем server entry через Vite SSR
const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')
const { html: appHtml } = await render(url)
// Вставляем рендер в шаблон
const finalHtml = template.replace('<!--app-html-->', appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(finalHtml)
} catch (e) {
vite.ssrFixStacktrace(e as Error)
next(e)
}
})
}
app.listen(5173)
}
createServer()
export default defineConfig({
plugins: [react()],
build: {
// Client build (по умолчанию)
outDir: 'dist/client',
},
})
// Для server build:
// vite build --ssr src/entry-server.tsx --outDir dist/server
import { renderToPipeableStream } from 'react-dom/server'
export function render(url: string, res: Response) {
const { pipe } = renderToPipeableStream(
<App />,
{
onShellReady() {
res.setHeader('content-type', 'text/html')
pipe(res)
},
onError(error) {
console.error(error)
},
}
)
}
// HTML начинает отправляться до окончания всего рендера!
Vite SSR ← Базовый слой для:
├── Nuxt 3 (Vue + SSR/SSG)
├── SvelteKit (Svelte + SSR/SSG/SPA)
├── Astro (Multi-framework SSR/SSG)
├── Remix (Vite mode) (React + SSR)
└── Qwik City (Qwik + SSR + Resumability)

Ниже — визуализация SSR пайплайна: от HTTP запроса до отображения страницы в браузере: