Server-Side Rendering (SSR)
O Slash oferece suporte completo a Server-Side Rendering (SSR), permitindo renderizar suas aplicações no servidor para melhorar a performance inicial, SEO e experiência do usuário.
O que é SSR?
Section titled “O que é SSR?”SSR (Server-Side Rendering) é o processo de renderizar sua aplicação no servidor, gerando HTML completo que é enviado ao navegador. Isso oferece vários benefícios:
- Performance: Conteúdo visível mais rápido (FCP - First Contentful Paint)
- SEO: Crawlers veem o conteúdo completo imediatamente
- Acessibilidade: Funciona mesmo com JavaScript desabilitado
- Experiência do Usuário: Reduz o tempo de carregamento percebido
renderToString()
Section titled “renderToString()”A função renderToString() renderiza seu componente para uma string HTML de forma síncrona.
Sintaxe
Section titled “Sintaxe”function renderToString(view: Child | (() => Child)): { html: string; state: Record<string, unknown>;}Parâmetros
Section titled “Parâmetros”- view: Componente ou função que retorna um componente a ser renderizado
Retorno
Section titled “Retorno”Retorna um objeto contendo:
- html: String HTML renderizada
- state: Objeto com os estados reativos serializados (para hidratação no cliente)
Exemplo Básico
Section titled “Exemplo Básico”import { renderToString, htmlString } from '@ezbug/slash'
const App = () => htmlString` <div class="app"> <h1>Hello, SSR!</h1> <p>Renderizado no servidor</p> </div>`
const { html, state } = renderToString(App)
console.log(html)// <div class="app">// <h1>Hello, SSR!</h1>// <p>Renderizado no servidor</p>// </div>
console.log(state)// {} (sem estados reativos neste exemplo)Exemplo com Estado Reativo
Section titled “Exemplo com Estado Reativo”import { renderToString, htmlString, createState } from '@ezbug/slash'
const Counter = () => { const count = createState({ value: 0 }) const { value } = count.get()
return htmlString` <div> <h2>Contador: ${value}</h2> <p>Renderizado com valor inicial</p> </div> `}
const { html, state } = renderToString(Counter)
console.log(html)// <div>// <h2>Contador: <!--reactive-start:s0-->0<!--reactive-end:s0--></h2>// <p>Renderizado com valor inicial</p>// </div>
console.log(state)// { s0: 0 }Note os marcadores de reatividade (<!--reactive-start:s0--> e <!--reactive-end:s0-->). Eles são usados durante a hidratação no cliente para reconectar os estados reativos.
htmlString - Template Tag para SSR
Section titled “htmlString - Template Tag para SSR”O htmlString é uma versão especial do template tag html otimizada para SSR. Ele renderiza diretamente para strings HTML.
Diferença entre html e htmlString
Section titled “Diferença entre html e htmlString”html (Cliente) | htmlString (Servidor) |
|---|---|
Retorna Node (DOM) | Retorna string (HTML) |
| Usa no navegador | Usa no servidor |
| Cria elementos reais | Cria strings HTML |
Uso Correto
Section titled “Uso Correto”// ❌ ERRADO: Usar html no servidorimport { html } from '@ezbug/slash'const App = () => html`<div>Não funciona no servidor</div>`
// ✅ CORRETO: Usar htmlString no servidorimport { htmlString } from '@ezbug/slash'const App = () => htmlString`<div>Funciona no servidor</div>`Escapando HTML
Section titled “Escapando HTML”O htmlString escapa automaticamente valores interpolados para prevenir XSS:
const malicious = '<script>alert("xss")</script>'const userInput = "João <script>alert()</script>"
const App = () => htmlString` <div> <p>${userInput}</p> </div>`
const { html } = renderToString(App)// <div>// <p>João <script>alert()</script></p>// </div>renderToStream()
Section titled “renderToStream()”Para aplicações maiores, renderToStream() oferece streaming SSR, enviando HTML em chunks incrementais.
Sintaxe
Section titled “Sintaxe”async function* renderToStream( view: Child | (() => Child)): AsyncGenerator<string, void, unknown>Benefícios do Streaming
Section titled “Benefícios do Streaming”- TTFB Melhorado: Primeiro byte chega mais rápido
- Renderização Progressiva: Navegador começa a renderizar antes do HTML completo
- Melhor Performance: Chunks de 16KB otimizados
- Menor Memory Usage: Processa em partes
Exemplo com Bun
Section titled “Exemplo com Bun”import { renderToStream, htmlString } from '@ezbug/slash'
const App = () => htmlString` <!DOCTYPE html> <html> <head> <title>Streaming SSR</title> </head> <body> <div id="app"> <h1>Conteúdo grande...</h1> ${Array.from({ length: 100 }).map((_, i) => htmlString`<p>Parágrafo ${i}</p>` )} </div> </body> </html>`
// Servidor BunBun.serve({ port: 3000, async fetch(req) { const stream = new ReadableStream({ async start(controller) { for await (const chunk of renderToStream(App)) { controller.enqueue(new TextEncoder().encode(chunk)) } controller.close() } })
return new Response(stream, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }) }})Exemplo com Node.js
Section titled “Exemplo com Node.js”import { renderToStream, htmlString } from '@ezbug/slash'import { createServer } from 'http'
const App = () => htmlString` <div> <h1>Streaming SSR com Node.js</h1> </div>`
createServer(async (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
for await (const chunk of renderToStream(App)) { res.write(chunk) }
res.end()}).listen(3000)Estado Serializado no Stream
Section titled “Estado Serializado no Stream”O renderToStream() automaticamente inclui um script com o estado serializado no final:
<div>...</div><script id="__SLASH_STATE__" type="application/json">{"s0":42,"s1":"active"}</script>Atributos Reativos
Section titled “Atributos Reativos”Quando você usa estados reativos em atributos, o Slash adiciona marcadores data-reactive-* para hidratação:
const isActive = createState({ value: true })
const Button = () => { const { value } = isActive.get() return htmlString`<button class=${value ? 'active' : ''}>Click</button>`}
const { html } = renderToString(Button)// <button class="active" data-reactive-class="s0">Click</button>Atributos Suportados
Section titled “Atributos Suportados”| Atributo | Marcador | Descrição |
|---|---|---|
class | data-reactive-class | Classes dinâmicas |
value | data-reactive-value | Valor de inputs |
checked | data-reactive-checked | Checkbox/radio checked |
| Outros | data-reactive-* | Atributos customizados |
Event Handlers
Section titled “Event Handlers”Event handlers (onClick, onInput, etc.) são ignorados durante SSR, pois só funcionam no cliente:
const Button = () => htmlString` <button onClick=${() => alert('click')}> Click me </button>`
const { html } = renderToString(Button)// <button>Click me</button>// (onClick foi removido)Os event handlers serão automaticamente reconectados durante a hidratação no cliente.
Void Elements
Section titled “Void Elements”O Slash trata corretamente void elements (elementos auto-fechados):
const Form = () => htmlString` <form> <input type="text" /> <br /> <img src="logo.png" /> </form>`
const { html } = renderToString(Form)// <form>// <input type="text">// <br>// <img src="logo.png">// </form>Lista de void elements: area, base, br, col, embed, hr, img, input, link, meta, param, source, track, wbr
Componentes Complexos
Section titled “Componentes Complexos”Renderizando Listas
Section titled “Renderizando Listas”type Todo = { id: number; text: string; done: boolean }
const todos = createState<Todo[]>({ value: [ { id: 1, text: 'Aprender SSR', done: true }, { id: 2, text: 'Fazer hydration', done: false } ]})
const TodoList = () => { const { value: items } = todos.get()
return htmlString` <ul class="todos"> ${items.map(todo => htmlString` <li class=${todo.done ? 'done' : ''}> <input type="checkbox" checked=${todo.done} /> <span>${todo.text}</span> </li> `)} </ul> `}
const { html, state } = renderToString(TodoList)Componentes Aninhados
Section titled “Componentes Aninhados”const Header = ({ title }: { title: string }) => htmlString` <header> <h1>${title}</h1> </header>`
const Footer = () => htmlString` <footer> <p>© 2026 My App</p> </footer>`
const Layout = ({ children }: { children: string }) => htmlString` <div class="layout"> <${Header} title="Minha App" /> <main>${children}</main> <${Footer} /> </div>`
const Page = () => Layout({ children: htmlString`<p>Conteúdo da página</p>`})
const { html } = renderToString(Page)Classes Dinâmicas
Section titled “Classes Dinâmicas”O Slash suporta múltiplos formatos para classes:
String
Section titled “String”const className = 'btn btn-primary'const Button = () => htmlString`<button class=${className}>Click</button>`const classes = ['btn', 'btn-primary', 'active']const Button = () => htmlString`<button class=${classes}>Click</button>`// <button class="btn btn-primary active">Click</button>Objeto
Section titled “Objeto”const classes = { btn: true, 'btn-primary': true, active: false}const Button = () => htmlString`<button class=${classes}>Click</button>`// <button class="btn btn-primary">Click</button>Style Objects
Section titled “Style Objects”Estilos inline podem ser objetos:
const styles = { color: 'red', fontSize: '16px', backgroundColor: '#f0f0f0'}
const Box = () => htmlString`<div style=${styles}>Styled box</div>`
const { html } = renderToString(Box)// <div style="color: red; font-size: 16px; background-color: #f0f0f0">Styled box</div>Melhorias de Performance
Section titled “Melhorias de Performance”Batching no Servidor
Section titled “Batching no Servidor”Embora batch() seja mais útil no cliente, você pode usá-lo no servidor para agrupar operações:
import { batch, createState, renderToString, htmlString } from '@ezbug/slash'
const data = createState({ count: 0, name: '' })
batch(() => { data.set({ count: 10, name: 'John' })})
const App = () => { const { count, name } = data.get() return htmlString`<div>${name}: ${count}</div>`}
const { html } = renderToString(App)Chunks Otimizados
Section titled “Chunks Otimizados”O renderToStream() divide o HTML em chunks de 16KB, otimizados para a maioria dos cenários:
// HTML grande é dividido automaticamenteconst largeContent = 'x'.repeat(50000)const App = () => htmlString`<div>${largeContent}</div>`
for await (const chunk of renderToStream(App)) { console.log(`Chunk size: ${chunk.length} bytes`) // Chunk size: 16384 bytes // Chunk size: 16384 bytes // ...}Integração com Frameworks
Section titled “Integração com Frameworks”Bun + Hono
Section titled “Bun + Hono”import { Hono } from 'hono'import { renderToString, htmlString } from '@ezbug/slash'
const app = new Hono()
app.get('/', (c) => { const App = () => htmlString` <!DOCTYPE html> <html> <body> <div id="app"> <h1>Hello from Hono + Slash SSR!</h1> </div> <script src="/client.js"></script> </body> </html> `
const { html, state } = renderToString(App)
return c.html(html + ` <script> window.__SLASH_STATE__ = ${JSON.stringify(state)} </script> `)})
export default appExpress.js
Section titled “Express.js”import express from 'express'import { renderToString, htmlString } from '@ezbug/slash'
const app = express()
app.get('/', (req, res) => { const App = () => htmlString` <div> <h1>Hello from Express + Slash SSR!</h1> </div> `
const { html, state } = renderToString(App)
res.send(` <!DOCTYPE html> <html> <body> ${html} <script> window.__SLASH_STATE__ = ${JSON.stringify(state)} </script> <script src="/client.js"></script> </body> </html> `)})
app.listen(3000)Checklist SSR
Section titled “Checklist SSR”- ✅ Use
htmlString(nãohtml) nos componentes do servidor - ✅ Use
renderToString()para SSR síncrono - ✅ Use
renderToStream()para streaming SSR (melhor performance) - ✅ Serialize o estado reativo e injete no HTML
- ✅ Implemente hidratação no cliente (veja Hydration)
- ✅ Event handlers são ignorados no servidor (reconectados no cliente)
- ✅ Atributos reativos recebem marcadores
data-reactive-* - ✅ HTML é escapado automaticamente para prevenir XSS
Próximos Passos
Section titled “Próximos Passos”- Aprenda sobre Hydration para reconectar o estado no cliente
- Explore Universal Data Loading para data fetching isomórfico
- Veja exemplos práticos no template slash-ssr