logo
dojo/framework

Build-time rendering

Build-time rendering (BTR) renders a route to HTML during the build process and in-lines critical CSS and assets needed to display the initial view. This allows Dojo to pre-render the initial HTML used by a route and inject it directly into the page immediately, resulting in many of the same benefits of server side rendering (SSR) such as performance gains and search engine optimization without the complexities of SSR.

Using BTR

First make sure index.html includes a DOM node with an id attribute. This node will be used by Dojo's virtual DOM to compare and render the application's HTML. BTR requires this setup so it can render the HTML generated at build time. This creates a very fast and responsive initial rendering of the route.

index.html

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <title>sample-app</title>
        <meta name="theme-color" content="#222127" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>

The application should then be mounted to the specified DOM node:

main.ts

const r = renderer(() => w(App, {}));
const domNode = document.getElementById('app') as HTMLElement;
r.mount({ registry, domNode });

The project's .dojorc configuration file should then be updated with the id of the root DOM node and routes to render at build time.

.dojorc

{
    "build-app": {
        "build-time-render": {
            "root": "app",
            "paths": [
                "#home",
                {
                    "path": "#comments/9999",
                    "match": ["#comments/.*"]
                }
            ]
        }
    }
}

This configuration describes two routes. A home route and a more complex comments route. The comments route is a more complex route with parameter data. A match is used to make sure that the build-time HTML created for this route is applied to any route that matches the regular expression.

BTR generates a screenshot for each of the paths rendered during build in the ./output/info/screenshots project directory.

History manager

Build time rendering supports applications that use either the @dojo/framework/routing/history/HashHistory or @dojo/framework/routing/history/StateHistory history managers. When using HashHistory, ensure that all paths are prefixed with a # character.

build-time-render feature flag

Build time rendering exposes a build-time-render feature flag that can be used to skip functionality that cannot be executed at build time. This can be used to avoid making fetch calls to external systems and instead provide static data that can be used to create an initial render.

if (has('build-time-render')) {
    const response = await fetch(/* remote JSON */);
    return response.json();
} else {
    return Promise.resolve({
        /* predefined Object */
    });
}

Dojo Blocks

Dojo provides a blocks system which can execute code in Node.js as part of the build time rendering process. The results from the execution are written to a cache that can then be transparently used in the same way at runtime in the browser. This opens up new opportunities to use operations that might be not possible or perform poorly in a browser.

For example, a Dojo Block module could read a group of markdown files, transform them into VNodes, and make them available to render in the application, all at build time. The result of this Dojo Block module is then cached into the application bundle for use at runtime in the browser.

A Dojo Block module gets used like any other meta in any Dojo widget. As a result there is no extensive configuration or alternative authoring patterns needed.

For example, a block module could read a text file and return the content to the application.

src/blocks/read-file.ts

import * as fs from 'fs';
import { resolve } from 'path';

export default (path: string) => {
    path = resolve(__dirname, path);
    return fs.readFileSync(path, 'utf8');
};

src/widgets/MyBlockWidget.tsx

import Block from '@dojo/framework/core/meta/Block';
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v } from '@dojo/framework/core/vdom';

import readFile from '../blocks/read-file';

export default class MyBlockWidget extends WidgetBase {
    protected render() {
        const message = this.meta(Block).run(readFile)('../content/hello-dojo-blocks.txt');
        return v('div', [message]);
    }
}

This widget runs the src/blocks/read-file.ts module at build time to read the contents of the given file. The contents are then used as a text node child within the widget's VDOM output.