I use Elmstatic to generate this site. As the number of posts crossed one hundred, Elmstatic became noticeably slow: it took several seconds to generate the site on every change.
The bulk of the time was spent in jsdom
-related code which is responsible for running elm.js
in order to generate HTML output.
My first thought was that perhaps if I reduced the size of elm.js
which is half a megabyte in its uncompressed form, it might make things faster. I added a step to run it through uglify
, but it turned out that it’s slower than just running unprocessed elm.js
.
Next, I looked at the code that generates the HTML:
const script = new Script(`
${elmJs}; let app = Elm.${pageOrPost.layout}.init({flags: ${JSON.stringify(pageOrPost)}})
`)
const dom = new JsDom(`<!DOCTYPE html><html><body></body></html>`, {
runScripts: "outside-only"
})
dom.runVMScript(script)
return "<!doctype html>" + R.replace(/script/g, "script", dom.window.document.body.innerHTML)
Looks like a lot of repeated work! For every page and post, I create a script which only differs in its last statement (let app = ...
). So I tried breaking up the script into two — one for the unchanging elm.js
(which would be reused) and another for instantiating the app — and then running them in sequence. It didn’t make any difference!
I also noticed that running jsdom
with runScripts: "outside-only"
is the slower option, but the problem is that using other modes makes it run the scripts included inside of the generated HTML — not at all what I want.
My next angle of attack was to avoid the wasteful recreation of the DOM object. Why not just clear out the body
and run the script for the next page? Well, that doesn’t work either — the HTML output I got was all mixed up.
I didn’t like the idea of a watch mode, as it was likely to add a lot of complexity. Ideally, I could just make elmstatic
run fast, and then use an external file watcher like chokidar
to watch for file changes and rebuild the site.
But, seeing as I was getting nowhere with speeding up the HTML generation, my only option was to start watching for file changes inside Elmstatic, in order to rebuild only what’s changed.
At first, I thought I would implement specific steps for every type of file change.
If a page changes, re-generate only that page. If a post changes, re-generate that post… but also rebuild the post list… and also the post list for every tag in the post. Hmm.
If a layout changes, rebuild the layout… and then re-generate any posts or pages using that layout… but that also means rebuilding post lists!
It was getting messy and complicated.
I put it aside for a while, until I realised that I could compromise on the statelessness of the code (Elmstatic is written in a mostly-functional JS), and cache the generated HTML.
If I tagged the generated HTML with file modification timestamps, I could easily identify which pages or posts need re-generating by comparing with current file modification timestamps when chokidar
reported a file change.
The rest of the process could remain unchanged: I would still wipe the output directory, and write/copy all the files.
This resulted in much simpler code. Aside from checking file modification timestamps and detecting changes to layouts (because layout changes imply a full rebuild, at least for now), the code remained pretty much the same.
I did take the opportunity to improve the error handling and logging a bit, and switched to Commander.js for handling command and options.
Live reload can be achieved by using Elmstatic watch mode together with browser-sync
(or another HTTP server that supports live reload):
browser-sync
:$ npm install -g browser-sync
$ elmstatic watch
browser-sync
in the output directory (<site dir>/_site
by default): $ cd _site
$ browser-sync start --server --files "." --no-ui --reload-delay 500 --reload-debounce 500
Now every time you save a file, Elmstatic will re-generate the site, and browser-sync
will in turn reload the page.
I hope you enjoy it!