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

20. Server-Side Rendering

Яша, SSR в Solid — это одна из его сильнейших сторон. Solid умеет не просто отдавать HTML со сервера, а делать это с потоковой передачей (streaming)! HTML начинает приходить клиенту сразу, пока данные ещё загружаются. Плюс — гидратация работает практически без оверхеда. Разберём всё! 🚀


Без SSR (SPA):
1. Браузер получает пустой HTML
2. Загружает JS бандл
3. Выполняет JS → рендерит HTML
4. Загружает данные
5. Отображает контент
❌ Медленный первый байт контента (FCP)
С SSR:
1. Сервер рендерит HTML сразу с данными
2. Браузер получает готовый HTML → пользователь видит контент
3. Загружается JS бандл
4. Гидратация → приложение становится интерактивным
✅ Быстрый FCP, хорошо для SEO

Самый простой способ — синхронный рендер в строку:

import { renderToString } from 'solid-js/web';
import App from './App';
// На сервере (Node.js):
const html = renderToString(() => <App />);
// Полный HTML
const fullPage = `
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="app">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`;
// Отправляем клиенту
response.send(fullPage);

⚠️ renderToString ждёт завершения всех createResource перед рендером. Если данные грузятся долго — клиент ждёт весь HTML целиком.


Вот где Solid блистает — потоковый рендер:

Express.js
import { renderToStream } from 'solid-js/web';
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.setHeader('Transfer-Encoding', 'chunked');
const stream = renderToStream(() => <App />);
// Сначала отправляем shell (заголовок)
res.write(`
<!DOCTYPE html>
<html>
<head><title>App</title></head>
<body><div id="app">
`);
// Streaming: каждый Suspense boundary стримится отдельно!
stream.pipe(res);
stream.on('end', () => {
res.write(`</div><script src="/bundle.js"></script></body></html>`);
res.end();
});
});

// С Suspense — каждый boundary стримится отдельно
function App() {
return (
<div>
{/* Этот блок отправится сразу */}
<Header />
{/* Этот блок придёт когда загрузятся пользователи */}
<Suspense fallback={<UserSkeleton />}>
<UserList />
</Suspense>
{/* Этот блок придёт когда загрузятся продукты */}
<Suspense fallback={<ProductSkeleton />}>
<ProductList />
</Suspense>
</div>
);
}
// Порядок получения HTML клиентом:
// 1. <Header /> — сразу
// 2. <UserSkeleton /> — сразу (пока данные грузятся)
// 3. Реальный <UserList /> — когда пришли данные
// 4. <ProductSkeleton /> → <ProductList /> — независимо!

// entry-server.tsx — сервер
import { renderToString } from 'solid-js/web';
import App from './App';
export async function render() {
return renderToString(() => <App />);
}
// entry-client.tsx — клиент (гидратация)
import { hydrate } from 'solid-js/web'; // не render!
import App from './App';
// hydrate сопоставляет серверный HTML с компонентами
// Минимальный JS overhead — только привязка обработчиков!
hydrate(() => <App />, document.getElementById('app')!);

Гидратация в Solid невероятно быстрая! Solid не перерисовывает DOM — он только привязывает реактивность к уже существующим DOM-узлам. В React виртуальный DOM заново обходится весь. Solid — только нужные точки.


import { isServer } from 'solid-js/web';
function Component() {
// Условный код в зависимости от среды
if (isServer) {
// Выполняется только на сервере
console.log('Это сервер');
}
// createEffect не выполняется на сервере!
createEffect(() => {
if (isServer) return; // Лишняя защита, но ясно показывает намерение
window.analytics.track('component-view');
});
return <div>Компонент</div>;
}
// Серверный код в утилитах
export function getBaseUrl() {
if (isServer) {
return process.env.BASE_URL || 'http://localhost:3000';
}
return window.location.origin;
}

// Это НЕ попадёт в браузер если использовать "use server"
import { query } from '@solidjs/router';
const getSecretData = query(async () => {
'use server'; // Директива — этот код только на сервере
const apiKey = process.env.PRIVATE_API_KEY; // Безопасно!
return fetch('https://private-api.com/data', {
headers: { Authorization: `Bearer ${apiKey}` }
}).then(r => r.json());
}, 'secret');
// Защищённый компонент
function SecretPanel() {
const data = createAsync(() => getSecretData());
return (
<Suspense fallback={<p>Загрузка...</p>}>
<pre>{JSON.stringify(data(), null, 2)}</pre>
</Suspense>
);
}

app.config.ts
// SolidStart Islands — только нужные компоненты гидрируются
// Остальная страница — чистый HTML, никакого JS!
export default defineConfig({ islands: true });
// components/Counter.tsx — "остров" интерактивности
// Этот компонент получит свой JS bundle
export default function Counter() {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}
// src/routes/index.tsx
import Counter from '~/components/Counter';
export default function Home() {
return (
<main>
{/* Статичный HTML — никакого JS */}
<h1>Заголовок</h1>
<p>Статичный текст...</p>
{/* "Остров" — получит JS для интерактивности */}
<Counter />
</main>
);
}