dojo dragon main logo

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 middleware or meta in a Dojo widget. For the Dojo build system to be able to identify and run a block module there are three requirements that must be met:

  1. The module must have a .block suffix, for example src/readFile.block.ts.
  2. The Block must only have a single default export
  3. Return values from blocks (from a promise resolution or as an immediate return) must be serializable to json

Other than these requirements there is no configuration or alternative authoring pattern required.

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

src/readFile.block.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 { create, tsx } from '@dojo/framework/core/vdom';
import block from '@dojo/framework/core/middleware/block';

import readFile from '../readFile.block';

const factory = create({ block });

export default factory(function MyBlockWidget({ middleware: { block } }) {
    const message = block(readFile)('../content/hello-dojo-blocks.txt');
    return <div>{message}</div>;
});

This widget runs the src/readFile.block.ts module at build time to read the contents of the given file to be used in the widget's render output.