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 thepreact-render-to-string package:
Basic Usage
- renderToString
- renderToStaticMarkup
The most common SSR function. Renders a component tree to an HTML string:This is a synchronous function that returns a complete HTML string.
Integration with Servers
Hydration
After sending server-rendered HTML, you need to “hydrate” it on the client. Hydration attaches event listeners and makes the app interactive.// server.js
import { renderToString } from 'preact-render-to-string';
import { App } from './App';
const html = renderToString(<App />);
// Send html to client...
// client.js
import { hydrate } from 'preact';
import { App } from './App';
// Hydrate the server-rendered HTML
hydrate(<App />, document.getElementById('app'));
The
hydrate function is implemented in src/render.js. It reuses the existing DOM instead of creating new elements:// From src/render.js:66-70
export function hydrate(vnode, parentDom) {
vnode._flags |= MODE_HYDRATE;
render(vnode, parentDom);
}
Streaming SSR
For large applications, streaming can improve Time To First Byte (TTFB):- renderToReadableStream (Web)
- renderToPipeableStream (Node)
Returns a ReadableStream for use in modern web environments:
Compat Layer
When usingpreact/compat, SSR functions are re-exported for React compatibility:
compat/server.d.ts
You can import from preact/compat/server just like react-dom/server:
Data Fetching
For SSR with data fetching, you’ll need to fetch data on the server before rendering:Best Practices
// 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>;
}
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>;
}
// 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>
`;
// server-bundle.js - for Node.js
import { renderToString } from 'preact-render-to-string';
// client-bundle.js - for browsers
import { hydrate } from 'preact';
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
Next Steps
TypeScript
Add type safety to your Preact application
Hooks
Master hooks for SSR-compatible components