Elmstatic: better performance and live reload with watch mode
2019-07-09

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.

I started looking at ways of making it run faster…

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 had to give in and add a watch mode…

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.

So now you can have live reload with Elmstatic:

Live reload can be achieved by using Elmstatic watch mode together with browser-sync (or another HTTP server that supports live reload):

  • Install browser-sync:
$ npm install -g browser-sync
  • Run Elmstatic in watch mode in your site directory:
$ elmstatic watch
  • Run 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!

Would you like to dive further into Elm?
📢 My book
Practical Elm
skips the basics and gets straight into the nuts-and-bolts of building non-trivial apps.
🛠 Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on.
Practical Elm