I have this question on SO:
https://stackoverflow.com/questions/50111841/precompile-handlebars-templates-on-express-server
with handlebars, you can "pre-compile" templates. I am guessing that it just makes it that much quicker to render the template with data, something like this:
const Handlebars = require('handlebars');
const string = fs.readFileSync('index.hbs');
const render = Handlebars.compile(string);
// you could then cache that render function and use it like so:
render({the:'data'});
so I know that Express is caching the template, because I am using
app.set('view cache', true);
however, how can I configure Express to pre-compile Handlebars templates, if it's not already doing that out-of-the-box?
That's what the view cache does: Express will read in the template and pass the contents to your template engine and saves the compiled function to use for future calls :+1:
ok cool, so I guess it knows what to do for different file extensions? .ejs and .hbs have to be treated differently right?
Yea, it just loads the relevant module. Typically you'll define it in your app setup what the extension maps to. Otherwise it maps to the module with the same name as the file extension.
@dougwilson where can I find the module that loads .hbs files in the Express codebase?
it looks like it's this file:
https://github.com/expressjs/express/blob/master/lib/view.js
and it looks like this is the function that gets cached:
// default engine export
var fn = require(mod).__express
if (typeof fn !== 'function') {
throw new Error('Module "' + mod + '" does not provide a view engine.')
}
opts.engines[this.ext] = fn
but I guess this is not sufficient to precompile a Handlebars template, there is significantly more optimization that we can do.
Can we discuss this one further? For example, with Handlebars, you'd want to cache this function:
const render = Handlebars.compile(string); // cache this render function
render({the:'data'});
my "fear" is that by only caching the __express export from Handlebars it is re-compiling the template for every res.render call.
My only hope is that require('hbs') will cache the compiled string after the first call:
https://github.com/pillarjs/hbs/blob/master/lib/hbs.js
it looks like that module does some caching.
So that is just loading up the entry point to the rendering, essentially where Express loads what would be the compile function itself. That is only once does, on the initial require(). From there, each of your res.render calls end up going through app.render: https://github.com/expressjs/express/blob/master/lib/application.js#L531
This function is what is looking at the view cache option and then, if enabled, saving the reference to the created view. That is the opportunity for your rendering module to precompile and save the compiled template. After that Express will call view.render(options, callback); on the view instance. It's up the to implementation of your renderer if that will cause a recompile every time or not.
If you're interested, this is one of our tests that tests the view caching mechanism in Express: https://github.com/expressjs/express/blob/19a2eeb47697feecae5960a726fb5b7ae2c7644b/test/app.render.js#L259-L287
The test is around app.render, but that is just what res.render calls (this way the test didn't need to start up an entire HTTP session).
You can see it's split between two things: the View constructor, which is where engines are given the file name and have the opportunity to read the file and compile the cache and then Express will just keep calling the .render method on the view engines's constructed object, passing in only the options.
If you roll that out for your Handlebars example, this would be a possible implementation (I typed this out really quick, forgive any typos):
function HandlebarsView (name) {
this.name = name
this.fn = null
}
HandlebarsView.prototype.render = function render (options, callback) {
if (this.fn) return callback(null, this.fn(options))
this.compile((err) => {
if (err) return callback(err)
callback(null, this.fn(options))
})
}
HandlebarsView.prototype.compile = function compile (callback) {
fs.readFile(this.name, 'utf8', (err, string) => {
if (err) return callback(err)
this.fn = Handlebars.compile(string)
callback()
})
}
Then the above would be attached in Express:
app.set('view cache', true);
app.set('view', HandlebarsView);
I know there are areas of improvements to the View system we want to get done for Express 5, for example getting the parts we do to be async instead of sync, so if you think there are any specific changes we need to make and what they are, we're open to hear 馃憤 even PRs targeted against the 5.0 branch (or master if you think they are backwards-compatible with existing engines) are welcome!