-
-
Notifications
You must be signed in to change notification settings - Fork 287
Server Renderer
@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.
npm install @happy-dom/server-renderer --save-devSee 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
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/"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.
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"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/"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
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.
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/"]]);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();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();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);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 buildThe 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.tsor if you have it installed
happy-dom-sr --config=./server-renderer.config.tsNext 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.userAgentand 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.serializableShadowRootstells 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-Controlto 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.
Help Packages