Skip to main content
Server-Side Rendering (SSR) lets you render Preact components to HTML on the server. This improves initial page load performance, SEO, and provides a better experience for users on slow connections.

Why Use SSR?

Better Performance

Users see content faster since HTML is sent immediately instead of waiting for JavaScript to load and execute.

Improved SEO

Search engines can index your content immediately without executing JavaScript.

Social Media

Social media crawlers can read your content for previews and cards.

Accessibility

Content is available even if JavaScript fails to load or is disabled.

Installation

SSR requires the preact-render-to-string package:
npm install preact-render-to-string
This package provides functions to render Preact components to HTML strings.

Basic Usage

The most common SSR function. Renders a component tree to an HTML string:
import { renderToString } from 'preact-render-to-string';
import { App } from './App';

// Render to HTML string
const html = renderToString(<App />);

console.log(html);
// Output: <div class="app">...</div>
This is a synchronous function that returns a complete HTML string.

Integration with Servers

import express from 'express';
import { renderToString } from 'preact-render-to-string';
import { App } from './App';

const app = express();

app.get('*', (req, res) => {
  const html = renderToString(<App url={req.url} />);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <title>My App</title>
        <link rel="stylesheet" href="/styles.css">
      </head>
      <body>
        <div id="app">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Hydration

After sending server-rendered HTML, you need to “hydrate” it on the client. Hydration attaches event listeners and makes the app interactive.
1
Server-side: Render to string
2
// server.js
import { renderToString } from 'preact-render-to-string';
import { App } from './App';

const html = renderToString(<App />);
// Send html to client...
3
Client-side: Hydrate
4
// client.js
import { hydrate } from 'preact';
import { App } from './App';

// Hydrate the server-rendered HTML
hydrate(<App />, document.getElementById('app'));
5
How Hydrate Works
6
The hydrate function is implemented in src/render.js. It reuses the existing DOM instead of creating new elements:
7
// From src/render.js:66-70
export function hydrate(vnode, parentDom) {
  vnode._flags |= MODE_HYDRATE;
  render(vnode, parentDom);
}
8
Hydration sets a flag that tells the diffing algorithm to preserve existing DOM nodes.
9
Reference: src/render.js:66-70
The component tree must be identical on server and client, or hydration will fail and Preact will re-render from scratch.

Streaming SSR

For large applications, streaming can improve Time To First Byte (TTFB):
Returns a ReadableStream for use in modern web environments:
import { renderToReadableStream } from 'preact-render-to-string/stream';
import { App } from './App';

// Modern web server (e.g., Cloudflare Workers)
export default {
  async fetch(request) {
    const stream = await renderToReadableStream(<App />);
    
    return new Response(stream, {
      headers: { 'Content-Type': 'text/html' }
    });
  }
};

Compat Layer

When using preact/compat, SSR functions are re-exported for React compatibility:
// From compat/server.d.ts
import { 
  renderToString,
  renderToStaticMarkup,
  renderToPipeableStream,
  renderToReadableStream 
} from 'preact-render-to-string';
Reference: compat/server.d.ts You can import from preact/compat/server just like react-dom/server:
import { renderToString } from 'preact/compat/server';

Data Fetching

For SSR with data fetching, you’ll need to fetch data on the server before rendering:
import express from 'express';
import { renderToString } from 'preact-render-to-string';
import { App } from './App';

const app = express();

app.get('/user/:id', async (req, res) => {
  // Fetch data on server
  const user = await fetch(`https://api.example.com/users/${req.params.id}`)
    .then(r => r.json());
  
  // Pass data to component
  const html = renderToString(<App user={user} />);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <body>
        <div id="app">${html}</div>
        <script>
          window.__INITIAL_DATA__ = ${JSON.stringify({ user })};
        </script>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

Best Practices

1
Avoid side effects during SSR
2
Don’t use browser-only APIs during rendering:
3
// Bad: localStorage is not available on server
function Component() {
  const theme = localStorage.getItem('theme');
  return <div className={theme}>Content</div>;
}

// Good: Check for browser environment
import { useState, useEffect } from 'preact/hooks';

function Component() {
  const [theme, setTheme] = useState('light');
  
  useEffect(() => {
    // Only runs on client
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) setTheme(savedTheme);
  }, []);
  
  return <div className={theme}>Content</div>;
}
4
Use useEffect for browser-only code
5
useEffect only runs on the client, making it safe for browser APIs:
6
import { useEffect, useState } from 'preact/hooks';

function Component() {
  const [width, setWidth] = useState(0);
  
  useEffect(() => {
    // Safe: only runs on client
    setWidth(window.innerWidth);
    
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return <div>Width: {width || 'calculating...'}px</div>;
}
7
Serialize data carefully
8
When passing data from server to client, avoid XSS vulnerabilities:
9
// Bad: Vulnerable to XSS
const html = `
  <script>
    window.__DATA__ = ${JSON.stringify(userData)};
  </script>
`;

// Good: Escape HTML in JSON
import serialize from 'serialize-javascript';

const html = `
  <script>
    window.__DATA__ = ${serialize(userData, { isJSON: true })};
  </script>
`;
10
Optimize bundle size
11
Keep your server bundle separate from client bundle:
12
// server-bundle.js - for Node.js
import { renderToString } from 'preact-render-to-string';

// client-bundle.js - for browsers
import { hydrate } from 'preact';
13
Handle errors gracefully
14
import { renderToString } from 'preact-render-to-string';

app.get('*', async (req, res) => {
  try {
    const html = renderToString(<App url={req.url} />);
    res.send(html);
  } catch (error) {
    console.error('SSR Error:', error);
    
    // Send fallback HTML that will render on client
    res.status(500).send(`
      <!DOCTYPE html>
      <html>
        <body>
          <div id="app"></div>
          <script src="/bundle.js"></script>
        </body>
      </html>
    `);
  }
});

Common Patterns

import { renderToString } from 'preact-render-to-string';
import { Router } from 'preact-router';
import { App } from './App';

app.get('*', (req, res) => {
  const html = renderToString(
    <Router url={req.url}>
      <App />
    </Router>
  );
  
  res.send(html);
});

Next Steps

TypeScript

Add type safety to your Preact application

Hooks

Master hooks for SSR-compatible components

Build docs developers (and LLMs) love