// Производительность серверного рендеринга в preact

В предыдущем посте я упоминал про медленный рендеринг preact на стороне ноды. Но после публикации в чатике индивеба усомнились в моих цифрах производительности. И я решил измерить производительность чуть лучше. Я писал про 200-300 мс, но это были цифры которые я увидел во вкладке network в браузере. Как оказалось, далеко не всё это время занимает выполнение моего кода. В данный момент после старта приложения первый http запрос отрабатывает в среднем за 100 мс. Последующие запросы за 50 мс. Это с выключенным кешированием. Проверял на главной странице с двумя постами. Я решил замерить три параметра. Это время рендеринга preact, извлечение css с помощью goober и сериализацию json. С измерением сериализации json возникла некоторая странность. Иногда она выполняется аномально долго, при этом там всегда одинаковые данные. Поэтому в результатах будет два варианта этого показателя, со всеми данными и с отфильтрованными аномальными значениями. Ну и да, сериализация выглядит вот так

const data = JSON.stringify(JSON.stringify(context.data));

Ну и собственно результаты

рендеринг приложения извлечение css сериализация json сериализация json отфлиьтрованая
Среднее значение первого запуска 15.337ms 0.534ms 0.357ms 0.357ms
Среднее значение последующих запусков 3.172ms 0.047ms 1.565ms 0.378ms
Минимальное значение первого запуска 14.423ms 0.135ms 0.205ms 0.205ms
Минимальное значение последующих запуска 1.934ms 0.037ms 0.145ms 0.145ms
Максимальное значение первого запуска 16.373ms 1.009ms 0.574ms 0.574ms
Максимальное значение последующих запуска 5.876ms 0.082ms 16.826ms 0.989ms

Хорошо видна оптимизирующая работа v8, поэтому первый запуск в среднем 5 раз медленнее чем последующие. К сериализации json это отношения не имеет, так как там работает нативный модуль. Ну и видимо никакое кеширование на самом деле не нужно, по крайней мере при таком маленьком приложении, ибо лишние 2-5 мс мало на что влияют. Интересно будет повторить эксперимент когда приложение будет немного побольше.

рендеринг приложения извлечение css сериализация json
1.0 16.373ms 0.787ms 0.574ms
1.1 2.930ms 0.047ms 7.589ms
1.2 3.009ms 0.054ms 0.257ms
1.3 2.777ms 0.040ms 0.254ms
1.4 2.408ms 0.037ms 3.570ms
1.5 2.760ms 0.042ms 0.253ms
2.0 14.658ms 0.540ms 0.475ms
2.1 4.221ms 0.064ms 0.415ms
2.2 3.233ms 0.082ms 0.654ms
2.3 2.828ms 0.045ms 0.276ms
2.4 2.847ms 0.042ms 3.212ms
2.5 2.869ms 0.050ms 0.272ms
3.0 15.878ms 0.135ms 0.238ms
3.1 4.624ms 0.052ms 0.257ms
3.2 3.268ms 0.055ms 0.989ms
3.3 2.638ms 0.041ms 0.425ms
3.4 5.426ms 0.040ms 0.665ms
3.5 2.475ms 0.050ms 0.261ms
4.0 15.353ms 0.198ms 0.295ms
4.1 2.739ms 0.048ms 16.826ms
4.2 2.312ms 0.044ms 0.150ms
4.3 2.215ms 0.038ms 0.314ms
4.4 2.121ms 0.040ms 0.152ms
4.5 1.934ms 0.037ms 0.145ms
5.0 14.423ms 1.009ms 0.205ms
5.1 5.129ms 0.069ms 0.341ms
5.2 3.054ms 0.041ms 0.278ms
5.3 2.796ms 0.040ms 0.493ms
5.4 5.876ms 0.046ms 0.386ms
5.5 2.833ms 0.039ms 0.702ms

// DSKR.DEV: как я пришёл к preact и SSR

Активность matinintim.com в декабре заставила меня задуматься о написании собственного блога. Идея витала давно, но тут появился отличный повод. Домен и сервер уже были, и я решил начать писать. Изначально я планировал использовать node.js и какой-нибудь шаблонизатор для рендеринга html на стороне сервера, но посмотрев на десяток популярных шаблонизаторы, я заплакал и понял что без jsx жить больше не могу. Вариантов с jsx я нашёл всего два. Один из них был генератором из jsx в html, то что нужно! Правда он не дружил с typescript, что было критично для меня. А второй был просто react'ом на бэкенде. Тогда-то я и понял что в любом случае придётся брать preact, ибо react слишком жирный. И надо будет делать сразу с SSR, ибо индивеб без SSR не работает. К счастью, прикрутить SSR к preact оказалось совсем не сложно. В первой итерации я решил использовать parcel для сборки фронтенда, так как это самый простой вариант. Ноду запускал используя ts-node-dev. На ноде решил писать на koa, ибо express немного надоел. Я уже привык к отсутствию body-parser'а в express'е, но то что в koa не будет роутера я совсем не ожила. Но это мелочи. Нода также раздаёт статику, и в проде и в деве. Вышло как-то так. Файл назвал server.ts


import Koa from 'koa';
import Router from '@koa/router';
import Static from 'koa-static';

import { renderApp } from './ssr';
const app = new Koa();

const router = new Router();


router.get(['/', '/p/*'], (ctx)=> ctx.body=renderApp(ctx.url));

app.use(Static('./dist'));
app.use(router.routes());

app.listen(4000);

Здесь я поднимаю koa сервер, раздающий статику и вызывающий функцию рендерящую фронтенд часть приложения.

Само preact приложение я написал в файле app.tsx

import { h } from 'preact';
import { setPragma } from 'goober';
import { Router, Link } from 'preact-router';

import { Grid } from './grid/component';

import { MainPage } from './page/main/component';
import { PostPage } from './page/post/component';

setPragma(h);

interface IProps {
  url?: string;
}

export const App = ({url}: IProps)=>{


  return (
    <Grid>
      <Router url={url}>
        <MainPage path="/"/>
        <PostPage path="/p/:year/:month/:day"/>
      </Router>
    </Grid>
  )
};

В целом здесь ничего интересного, обычной preact, обычный роутер, которому можно передать текущий url через пропсы для правильной работы на сервере. Я использую новенькую библиотека для css in js goober, у неё очень маленький рантайм, минималистичный api и она умеет в SSR.

Дальше я написал файл ssr.tsx который выполняет всю магию серверсайд рендеринга:

import { h } from 'preact';
import render from 'preact-render-to-string';
import { extractCss } from 'goober';

import { App } from './app';

const cache = new Map<string, string>();

const realRender = (url: string) => {
  const app = render(<App url={url} />);
  const style = extractCss();

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>DSKR.DEV</title>
  <link rel="microsub" href="https://aperture.p3k.io/microsub/448">
  <link rel="authorization_endpoint" href="https://indieauth.com/auth">
  <link rel="token_endpoint" href="https://tokens.indieauth.com/token">
  <style>${style}</style>
</head>
<body>
  <div id="app">${app}</div>
  <script src="/web.js"></script>
</body>
</html>
`;
};

export const renderApp = (url: string) => {
  if(cache.has(url)) {
    return cache.get(url);
  }
  const app = realRender(url);
  process.nextTick(() =>cache.set(url, app));
  return app;
};

Функция realRender собственно и выполняет сам рендеринг, там ничего сложного, используем preact-render-to-string для рендеринга html, extractCss для получения стилей из goober, а дальше вставляется всё в html.

Ну а функция renderApp тупо кэширует результат функции realRender. Так как рендер preact приложения, даже такого маленького, не такая уж и простая задача и занимает 200—300 мс. А так мы выполняем рендер один раз, а дальше отдаёт готовый html, учитывая что это блог, это идеальное решение.

А точку входа в веб-приложение я разместил в файле web.tsx


import { h, render } from "preact";
import { App } from "./app";

render(<App />, document.body, document.getElementById("app") || undefined);

Я просто рендерю preact приложение.

В результате я получил простейшее приложение на preact с SSR, css in js и typescript. Ну а в следующих частях я расскажу как я добавил стейт, научился парсить markdown, подсвечивать синтаксис в блоках кодах, и поднял свой micropub server (Надеюсь, я когда-нибудь и это смогу сделать).

// Hello world indieweb

Hello world! Это первый пост в индивебе