Server-Side Rendering с Hyperapp

Hyperapp — это минималистичный фреймворк для построения реактивных пользовательских интерфейсов, ориентированный на небольшие и быстрые приложения. Одним из важных аспектов современных веб-приложений является Server-Side Rendering (SSR), позволяющий формировать HTML на сервере до отправки клиенту. SSR повышает скорость загрузки, улучшает SEO и обеспечивает начальное отображение страницы без ожидания загрузки JavaScript на клиенте.

Hyperapp, благодаря своей легковесной архитектуре, позволяет реализовать SSR достаточно просто. Важная особенность — состояние приложения и функции отображения (view) должны быть детерминированы, чтобы серверный рендер точно соответствовал клиентскому.


Подготовка приложения к SSR

Для работы SSR необходимо разделить логику приложения на три ключевых компонента:

  1. State — глобальное состояние приложения.
  2. Actions — функции изменения состояния.
  3. View — функция, которая на основе состояния возвращает виртуальный DOM.
import { h, app } from "hyperapp";

const state = {
  count: 0
};

const actions = {
  increment: value => state => ({ count: state.count + value })
};

const view = (state, actions) => 
  h("div", {}, [
    h("h1", {}, `Счётчик: ${state.count}`),
    h("button", { onclick: () => actions.increment(1) }, "Увеличить")
  ]);

Рендеринг на сервере

Hyperapp предоставляет функцию app для запуска приложения на клиенте, однако для SSR нужно использовать отдельный подход — генерацию HTML с помощью h и преобразование виртуального DOM в строку. Это достигается через утилиты вроде hyperapp-render или кастомной функции.

Простейший пример рендеринга HTML на сервере:

import { h } from "hyperapp";
import { render } from "hyperapp-render"; // библиотека для рендера на сервере

const state = { count: 5 };
const view = (state) =>
  h("div", {}, [
    h("h1", {}, `Счётчик: ${state.count}`)
  ]);

const html = render(view(state));
console.log(html); // <div><h1>Счётчик: 5</h1></div>

Ключевой момент: серверный рендер возвращает чистый HTML, который затем можно вставить в шаблон страницы и отправить клиенту.


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

Для полноценного SSR Hyperapp часто интегрируется с сервером Node.js. Пример маршрута Express с генерацией HTML:

import express from "express";
import { h } from "hyperapp";
import { render } from "hyperapp-render";

const appServer = express();

const state = { count: 10 };
const view = state => h("div", {}, `Счётчик: ${state.count}`);

appServer.get("/", (req, res) => {
  const html = render(view(state));
  res.send(`
    <!DOCTYPE html>
    <html lang="ru">
      <head><title>Hyperapp SSR</title></head>
      <body>${html}</body>
    </html>
  `);
});

appServer.listen(3000);

Особенность: сервер рендерит страницу один раз при запросе, формируя начальный HTML. Клиентский JavaScript затем «гидратирует» страницу, добавляя интерактивность.


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

После SSR сервер отправляет готовый HTML. Для сохранения реактивности Hyperapp требуется гидратация, то есть связывание уже отрендеренного DOM с клиентским состоянием.

import { app } from "hyperapp";

const state = { count: 10 };
const actions = {
  increment: value => state => ({ count: state.count + value })
};
const view = (state, actions) =>
  h("div", {}, [
    h("h1", {}, `Счётчик: ${state.count}`),
    h("button", { onclick: () => actions.increment(1) }, "Увеличить")
  ]);

app({ init: state, view, node: document.body, actions });

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


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

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

const fetchData = async () => {
  const response = await fetch("https://api.example.com/data");
  return await response.json();
};

appServer.get("/", async (req, res) => {
  const data = await fetchData();
  const html = render(state => h("div", {}, `Данные: ${JSON.stringify(data)}`));
  res.send(html);
});

Преимущество: страница сразу содержит данные, что ускоряет отображение и улучшает SEO.


Поддержка маршрутизации

Для многостраничных приложений SSR в Hyperapp часто используют собственную маршрутизацию. Сервер обрабатывает URL и рендерит соответствующий view:

const routes = {
  "/": () => h("h1", {}, "Главная"),
  "/about": () => h("h1", {}, "О нас")
};

appServer.get("*", (req, res) => {
  const viewFn = routes[req.path] || (() => h("h1", {}, "404"));
  const html = render(viewFn());
  res.send(html);
});

Маршрутизация на клиенте затем синхронизирует состояние, чтобы ссылки оставались интерактивными без перезагрузки страницы.


Оптимизация и кэширование

Для увеличения производительности SSR-страниц полезно:

  • Кэшировать HTML для популярных маршрутов.
  • Минимизировать состояние: рендерить только необходимые данные.
  • Использовать lazy-loading для тяжёлых компонентов, чтобы сервер не блокировался при загрузке больших данных.

Эти меры позволяют Hyperapp эффективно работать в SSR-режиме даже для сложных приложений.


Итоговые принципы SSR в Hyperapp

  1. Состояние должно быть детерминировано и одинаково на сервере и клиенте.
  2. HTML формируется на сервере через рендеринг виртуального DOM.
  3. Клиентская гидратация обеспечивает интерактивность без перерисовки всего DOM.
  4. Асинхронные данные должны загружаться на сервере для полного отображения страницы.
  5. Маршрутизация и кэширование улучшают производительность и масштабируемость.

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