Renderização no servidor

Renderização no servidor

Renderize componentes em HTML no servidor e hidrate no navegador, com o mesmo código dos dois lados.

Configuração

bash
npm install @velozjs/dom @velozjs/ssr
npm install -D vite vite-plugin-veloz

Com o vite-plugin-veloz, seus imports de @velozjs/dom apontam para @velozjs/ssr durante o SSR (em dev com ssrLoadModule e em vite build --ssr), então os componentes não precisam saber onde rodam.

Entrada do servidor

src/entry-server.tsx
import { renderToString } from '@velozjs/ssr';
import { App } from './App';

export function render(url: string) {
  return renderToString(() => <App url={url} />); // { html, css }
}

HTML base

index.html
<!doctype html>
<html lang="pt-BR">
  <head>
    <!--ssr-css-->
  </head>
  <body>
    <div id="app"><!--ssr-outlet--></div>
    <script type="module" src="/src/entry-client.tsx"></script>
  </body>
</html>
server.js
const { html, css } = render(req.url);
res.end(
  template
    .replace('<!--ssr-css-->', `<style data-veloz-css>${css}</style>`)
    .replace('<!--ssr-outlet-->', html)
);

Entrada do cliente

src/entry-client.tsx
import { hydrate } from '@velozjs/dom';
import { App } from './App';

hydrate(() => <App url={location.pathname} />, document.getElementById('app')!);

Dados assíncronos

O renderToString é síncrono: recursos que ainda estão carregando renderizam o fallback do Suspense. Para enviar a página com os dados, use o renderToStringAsync:

ts
import { renderToStringAsync } from '@velozjs/ssr';

const { html, css, script } = await renderToStringAsync(() => <App url={req.url} />, {
  timeout: 5000,
});

res.end(
  template
    .replace('<!--ssr-css-->', `<style data-veloz-css>${css}</style>`)
    .replace('<!--ssr-outlet-->', html + script) // o script precisa rodar antes da entrada do cliente
);

Ele renderiza, espera as buscas pendentes e renderiza de novo com os valores, repetindo quando uma requisição depende de outra. O script define os valores para o hydrate(), que inicia cada recurso com eles em vez de buscar.

OpçãoPadrão
timeoutnenhumrejeita se os dados demorarem mais (ms)
maxPasses10limite para cadeias de requisições dependentes
noncenenhumnonce de CSP para o script de dados

Os valores precisam ser serializáveis em JSON; cargas que falharam são enviadas com a mensagem de erro. O JSON é escapado para não conseguir sair do <script>.

Como a hidratação funciona

  • O servidor dá a cada elemento uma chave data-hk na ordem de criação e envolve cada região dinâmica em comentários <!--[-->…<!--]-->.
  • O hydrate() indexa as chaves dentro do container uma vez e cria a mesma árvore: os elementos são reaproveitados em vez de criados, eventos e effects são ligados e as chaves são removidas.
  • As regiões dinâmicas se ligam aos marcadores, então o texto estático ao lado de texto dinâmico fica como o servidor enviou.
  • A hidratação é limitada ao container, então várias raízes independentes na mesma página funcionam. Este site faz isso: cada parte interativa é uma raiz pequena.

O que muda no servidor

  • createEffect e onMount não rodam (servem para efeitos no navegador), então o código dentro deles pode usar window e document à vontade. Memos e recursos rodam. Handlers de evento não são impressos.
  • innerHTML não é impresso (não há sanitizador de DOM no servidor); o cliente o define, sanitizado, na hidratação.
  • O Portal não renderiza nada; o conteúdo é criado no cliente.
  • O renderToStream() devolve a mesma saída como stream de bytes, que você passa para new Response().

Sites estáticos

A mesma API pré-renderiza páginas no build: renderize cada rota com renderToString (ou renderToStringAsync) e grave o HTML no disco. Este site é feito assim, com só as partes interativas hidratadas.