Skip to content

Server Renderer

David Ortner edited this page Jan 27, 2026 · 33 revisions

@happy-dom/server-renderer is a tool for using Happy DOM for server-side rendering (SSR) or static site generation (SSG).

The benefit of using this tool compared to other SSR and SSG solutions, is that it is not tied to a specific framework. For example, you can use React, Vue, Angular and Lit on the same page. The page is rendered as a whole and outputted as HTML.

This tool uses a worker pool by default to render multiple pages in parallel. Each worker can also render multiple pages in parallel that shares resources.

Installation

npm install @happy-dom/server-renderer --save-dev

Usage

Command Line (CLI)

See all available command line arguments in the CLI Documentation.

This example will output the result to the file ./happy-dom/render/gb/en/index.html.

Note that JavaScript is disabled by default. To enable JavaScript, use the flag --javascript. A VM Context is not an isolated environment, and if you run untrusted JavaScript code you are at risk of RCE (Remote Code Execution) attacks. Read more about risks and recommended security precautions here.

npx @happy-dom/server-renderer --javascript "https://example.com/gb/en/"

or if you have it installed

happy-dom-sr --javascript "https://example.com/gb/en/"
Show more command line examples

Configuration File

This will output the result to the file ./happy-dom/render/gb/en/index.html.

Read more about the configuration file in the CLI Documentation.

happy-dom-sr --config=<path-to-config-file> "https://example.com/gb/en/"

Virtual Server

This will setup a virtual server that serves the content from the directory "./build" for requests towards https://example.com/{cc}/{lc}/.

The result will be saved to the file "./happy-dom/render/gb/en/index.html".

happy-dom-sr --javascript --browser.fetch.virtualServer="https:\/\/example\.com\/[a-z]{2}\/[a-z]{2}\/"|"./build" "https://example.com/gb/en/"

Read more about virtual servers in the API Documentation for Happy DOM.

Proxy Server

This will start a proxy server that proxies requests from https://localhost:3000 to https://example.com.

happy-dom-sr --javascript --server --server.targetOrigin="https://example.com"

Debugging

Some pages may have never ending timer loops causing the rendering to never complete or there may be other issues preventing the page from rendering correctly. You can enable debugging by adding the flag "--debug".

Setting the log level to 4 (debug) will provide with even more information (default is 3).

Note that debug information is collected after the page rendering has timed out. The default rendering timeout is 30 seconds and can be changed using the flag "--render.timeout".

happy-dom-sr --javascript --debug --logLevel=4 "https://example.com/gb/en/"

JavaScript

See all available configuration options in the API Documentation.

Note that JavaScript is disabled by default. To enable JavaScript, set the configuration browser.enableJavaScriptEvaluation to true. A VM Context is not an isolated environment, and if you run untrusted JavaScript code you are at risk of RCE (Remote Code Execution) attacks. Read more about risks and recommended security precautions here.

import { ServerRenderer } from "@happy-dom/server-renderer";

const renderer = new ServerRenderer({
	// Your configuration options
	browser: {
		enableJavaScriptEvaluation: true,
	},
});

const results = await renderer.render(["https://example.com/gb/en/"]);

// The rendered HTML
console.log(results[0].url);
console.log(results[0].content);
Show more Javascript examples

Virtual Server

This will setup a virtual server that serves the content from the directory "./build" for requests towards https://example.com/{cc}/{lc}/.

The result will be saved to the files "./index_gb.html" and "./index_us.html".

import { ServerRenderer } from "@happy-dom/server-renderer";

const renderer = new ServerRenderer({
	browser: {
		enableJavaScriptEvaluation: true,
		fetch: {
			virtualServers: [
				{
					url: /https:\/\/example\.com\/[a-z]{2}\/[a-z]{2}\//,
					directory: "./build",
				},
			],
		},
	},
});

await renderer.render([
	{
		url: "https://example.com/gb/en/",
		outputFile: "./index_gb.html",
		headers: { "X-Test": "true" },
	},
	{ url: "https://example.com/us/en/", outputFile: "./index_us.html" },
]);

Read more about virtual servers in the API Documentation for Happy DOM.

Debugging

Some pages may have never ending timer loops causing the rendering to never complete or there may be other issues preventing the page from rendering correctly. You can enable debugging by setting the configuration debug to "true".

Setting the log level to 4 (debug) will provide with even more information (default is 3).

Note that debug information is collected after the page rendering has timed out. The default rendering timeout is 30 seconds and can be changed using the flag "--render.timeout".

import {
	ServerRenderer,
	ServerRendererLogLevelEnum,
} from "@happy-dom/server-renderer";

const renderer = new ServerRenderer({
	browser: {
		enableJavaScriptEvaluation: true,
	},
	debug: true,
	logLevel: ServerRendererLogLevelEnum.debug,
	render: {
		timeout: 10000, // 10 seconds
	},
});

const results = await renderer.render([["https://example.com/gb/en/"]]);

Keeping the Workers Alive

When used on a server, you may want to keep the workers alive between renders to avoid the overhead of starting new workers.

import { ServerRenderer } from "@happy-dom/server-renderer";

const results = await renderer.render(
	["https://example.com/gb/en/", "https://example.com/us/en/"],
	{ keepAlive: true },
);

for (const result of results) {
	console.log(result.url);
	console.log(result.content);
}

// When you are done with rendering, close the workers
await renderer.close();

Proxy Server

Start a proxy server that proxies requests from https://localhost:3000 to https://example.com.

import { ServerRendererServer } from "@happy-dom/server-renderer";

const server = new ServerRendererServer({
	browser: {
		enableJavaScriptEvaluation: true,
	},
	server: {
		serverURL: "https://localhost:3000/gb/en/",
		targetOrigin: "https://example.com",
	},
});
await server.start();

Render HTML strings

Rendering HTML strings instead of URLs is also supported.

import { ServerRenderer } from "@happy-dom/server-renderer";

const results = await renderer.render([
	{
		html: "<html><body>Hello world!</body></html>",
		url: "https://localhost:3000",
	},
]);

console.log(results[0].url);
console.log(results[0].content);

Advanced

Detecting Server Environment

You may want to detect if the code is running in a server environment or in a browser. You can do this by checking navigator.userAgent.

function isServerRendering() {
	return navigator.userAgent.includes("HappyDOM");
}
Static Site Generation (SSG) with Vite or Webpack

This example shows how to statically render (SSG) your application after it has been built using Vite or Webpack.

The easiest way to simulate a server environment is to use the browser setting fetch.virtualServers to serve the built files using a virtual server. Read more about virtual servers in the API Documentation for Happy DOM.

We will use Vite in this example, but the same principles apply to Webpack.

The Vite config file looks like this:

import { defineConfig } from "vite";

export default defineConfig({
	build: {
		rollupOptions: {
			input: {
				home: "./src/index.html",
				about: "./src/about/index.html",
			},
		},
	},
});

We will build the application using the command:

npx vite build

The built files are outputted to the directory ./dist.

We want to deploy the rendered files to a server where the home page is available at https://example.com/{cc}/{lc}/ and the about page at https://example.com/{cc}/{lc}/about/. We want the application to be able to support translations by path, so country and language code has also been added to the URL ({cc} and {lc}).

Lets create a config file for the server-renderer called server-renderer.config.ts:

import type { IOptionalServerRendererConfiguration } from "@happy-dom/server-renderer";

export default <IOptionalServerRendererConfiguration>{
	browser: {
		enableJavaScriptEvaluation: true,
		fetch: {
			virtualServers: [
				{
					url: /https:\/\/example\.com\/[a-z]{2}\/[a-z]{2}\//,
					directory: "./dist",
				},
			],
		},
	},
	render: {
		// Serialize shadow DOMs with the "serializable" option set to true
		// This is only needed if you want to render Web Components with shadow DOMs
		serializableShadowRoots: true,
	},
	urls: [
		// Home page for US and SE
		"https://example.com/us/en/",
		"https://example.com/se/sv/",
		// About page for US and SE
		"https://example.com/us/en/about/",
		"https://example.com/se/sv/about/",
	],
};

Now we can render the pages using the following CLI command:

npx @happy-dom/server-renderer --config=./server-renderer.config.ts

or if you have it installed

happy-dom-sr --config=./server-renderer.config.ts

Next and final step is to upload the rendered files from the directory ./happy-dom/render/ to your server, together with the static assets from the directory ./dist/static/.

Server-Side Rendering of Web Components in SvelteKit

SvelteKit doesn't support server-side rendering of Web Components out of the box. However, by using @happy-dom/server-renderer you can add support for server-side rendering of Web Components in SvelteKit.

import { sequence } from "@sveltejs/kit/hooks";
import type { Handle } from "@sveltejs/kit";
import {
	ServerRenderer,
	ServerRendererModeEnum,
} from "@happy-dom/server-renderer";

const renderer = new ServerRenderer({
	browser: {
		module: {
			// Enables node modules resolution in Happy DOM for the setup script imports.
			resolveNodeModules: {
				url: "/$root/node_modules/",
				directory: "./node_modules/",
			},
		},
	},
	render: {
		// The "page" render mode only renders one page per Worker and doesn't use a VM context for each page (some JavaScript execution environments doesn't support VM context).
		// When the "keepAlive" option is used, it will reuse resources (such as ESM modules) between renders and can therefore improve performance.
		// Be aware that ESM modules that are reused can have memory leaks in them if they are not designed to be reused.
		mode: ServerRendererModeEnum.page,
		// Enables Shadow DOM support for all Web Components.
		// It's also possible to use "serializableShadowRoots: true" instead to only serialize Shadow DOM's configured with "serializable: true".
		allShadowRoots: true,
		// JavaScript evaluation is disabled by default in Happy DOM and we want to keep it disabled as running the Svelte client-side code will not work during this stage.
		// We can instead use a setup script to only import the Web Components we need.
		setupScript: `
			import "@web-component/button";
			import "@web-component/carousel";
			import "@web-component/icon";
		`,
	},
});

const handleWebComponentSSR: Handle = async ({ event, resolve }) => {
	const response = await resolve(event, {
		// Hook into the page rendering to transform the HTML
		transformPageChunk: async ({ html }) => {
			// Render the page HTML using Happy DOM Server Renderer
			// We use the "keepAlive: true" option to keep the render Worker's alive between calls for better performance
			const results = await renderer.render(
				[{ url: event.url.href, html }],
				{ keepAlive: true },
			);

			// Return the transformed HTML content
			return results[0].content;
		},
	});
	return response;
};

export const handle = sequence(handleWebComponentSSR);
Add meta tags for SEO

When using server rendering, you may want to add meta tags for SEO purposes. You can do this by adding the meta tags to the head of your HTML document.

The meta tags will be available in the rendered HTML if they are applied by a script in the application being rendered.

const meta = document.createElement("meta");
meta.name = "description";
meta.content = "Your description here";
document.head.appendChild(meta);
Performance Tips

Adding server-side rendering to your application can reduce the time to first meaningful paint, but will by default increase the overall load for the client. Here are some tips to improve the performance of your server-side rendering:

  • CSS files: Using CSS files instead of inline styles will reduce the size of the HTML document, however, it may cause layout shifts if the CSS file is large and takes time to load. You can mitigate this by inlining critical CSS and loading the rest of the CSS asynchronously.
  • Only render what is needed: Avoid rendering parts of the page that are not needed. You can detect if it is a server environment by checking navigator.userAgent and conditionally render parts of the page.
  • Minimize JavaScript: Minimize the amount of JavaScript that is executed on the client on first load. This will reduce the time to interactive and improve the overall performance of the page. You can use code splitting and lazy loading to achieve this.
  • Micro Frontends using Web Components: One way to structure your application is to use micro frontends using Web Components. The configuration render.serializableShadowRoots tells the server render to serialize shadow DOMs with the serializable option set to true.
  • Cache rendered pages: You can use a CDN or a caching layer to achieve this. Use cache headers like Cache-Control to control how long the rendered HTML should be cached.
  • Avoid costly DOM API calls: Avoid using costly DOM API calls during the rendering process (such as getComputedStyle()). These calls can slow down the rendering process significantly. You can use the "inspect" configuration option and connect to the inspector using Chrome DevTools to identify any performance bottlenecks during rendering.
  • Page vs Browser mode: "page" mode reused resources (such as compiled ESM modules) between renders, while "browser" mode creates a new isolated environment for each render. If you don't need full isolation between renders, using "page" mode may improve performance. Be aware that ESM modules that are reused can have memory leaks in them if they are not designed to be reused.

Clone this wiki locally