Серверный рендеринг с маршрутизацией

Серверный рендеринг (SSR) в Inferno обеспечивает предварительную генерацию HTML на сервере перед отправкой клиенту. Это повышает производительность, улучшает SEO и сокращает время до первого рендеринга (Time to First Paint). В сочетании с маршрутизацией SSR позволяет отображать разные страницы приложения в зависимости от URL ещё до загрузки JavaScript на клиенте.


Основы серверного рендеринга

Inferno поддерживает серверный рендеринг через пакет inferno-server. Основной метод — renderToString, который принимает React-подобный компонент и возвращает строку HTML.

Пример базового рендеринга:

import { renderToString } from 'inferno-server';
import { createElement } from 'inferno';

const App = () => <div>Привет, Inferno SSR!</div>;

const html = renderToString(<App />);
console.log(html); // "<div>Привет, Inferno SSR!</div>"

Ключевые моменты:

  • renderToString используется для генерации HTML на сервере.
  • Серверный HTML можно отправлять в ответе на HTTP-запрос.
  • После загрузки на клиенте Inferno подхватывает уже сгенерированный HTML с помощью hydrate.

Подключение маршрутизации

Для маршрутизации на сервере используется пакет inferno-router или более лёгкая альтернатива inferno-router-component. В серверном контексте маршрутизатор должен быть синхронизирован с URL запроса, чтобы вернуть корректный HTML для каждой страницы.

Пример интеграции маршрутизации:

import { renderToString } from 'inferno-server';
import { createElement } from 'inferno';
import { Router, Route, Link, createMemoryHistory } from 'inferno-router';

const Home = () => <div>Главная</div>;
const About = () => <div>О нас</div>;

function render(url) {
    const history = createMemoryHistory({ initialEntries: [url] });

    const app = (
        <Router history={history}>
            <Route path="/" component={Home} exact />
            <Route path="/about" component={About} />
        </Router>
    );

    return renderToString(app);
}

console.log(render("/about")); // "<div>О нас</div>"

Ключевые моменты:

  • createMemoryHistory используется вместо browserHistory, так как на сервере нет окна браузера.
  • initialEntries определяет URL, который необходимо отрендерить.
  • Каждое обращение к серверу вызывает генерацию HTML в зависимости от маршрута.

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

После получения сгенерированного сервером HTML необходимо «подхватить» его на клиенте. Для этого используется hydrate вместо render:

import { hydrate } from 'inferno';
import { Router } from 'inferno-router';
import { createBrowserHistory } from 'history';
import App from './App';

const history = createBrowserHistory();

hydrate(
    <Router history={history}>
        <App />
    </Router>,
    document.getElementById('root')
);

Особенности:

  • hydrate сохраняет существующий DOM и присоединяет события.
  • Серверный HTML должен точно соответствовать компонентам на клиенте, иначе будут расхождения и предупреждения.

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

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

Пример асинхронного рендеринга:

const fetchData = () => new Promise(resolve => setTimeout(() => resolve("Данные с сервера"), 100));

const DataPage = ({ data }) => <div>{data}</div>;

async function render(url) {
    const data = await fetchData();
    return renderToString(<DataPage data={data} />);
}

render("/data").then(html => console.log(html)); // "<div>Данные с сервера</div>"

Ключевые моменты:

  • Асинхронная загрузка данных требует использования async/await.
  • Сервер генерирует HTML только после завершения всех промисов.
  • На клиенте можно повторно использовать загруженные данные для гидратации, чтобы избежать лишних запросов.

Обработка ошибок и редиректов

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

import { Redirect } from 'inferno-router';

function App({ url }) {
    if (url === "/old-page") {
        return <Redirect to="/new-page" />;
    }
    return <div>Страница</div>;
}

const html = renderToString(<App url="/old-page" />);

Особенности:

  • Redirect позволяет серверу возвращать правильный статус и заголовок Location.
  • Для 404 страниц можно использовать маршрут с * или специальную компоненту NotFound.

Интеграция с Express

Чаще всего SSR на Inferno реализуется через Node.js и Express. Пример базового сервера:

import express from 'express';
import { renderToString } from 'inferno-server';
import { createMemoryHistory } from 'history';
import { Router, Route } from 'inferno-router';
import App from './App';

const server = express();

server.get('*', (req, res) => {
    const history = createMemoryHistory({ initialEntries: [req.url] });
    const html = renderToString(
        <Router history={history}>
            <App />
        </Router>
    );
    res.send(`<!DOCTYPE html><html><body><div id="root">${html}</div><script src="/bundle.js"></script></body></html>`);
});

server.listen(3000);

Ключевые моменты:

  • Маршруты Express обрабатывают все запросы (*) и передают URL в маршрутизатор Inferno.
  • Сгенерированный HTML оборачивается в стандартный шаблон страницы.
  • На клиенте происходит гидратация через hydrate.

Рекомендации по производительности

  • Минимизировать размер серверного HTML и использовать gzip сжатие.
  • Кэшировать результаты SSR для неизменяемых страниц.
  • Ограничивать использование тяжёлых сторонних библиотек на сервере, чтобы уменьшить время рендеринга.
  • Предварительно загружать данные для маршрутов с помощью промисов и async/await для снижения времени TTFB.

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