Created August 10, 2021
Updated August 16, 2021

Avoid Work in the Browser

One of the main principles in [Jamstack] is to do as much work as possible in the build step instead of in the user's browser.

Example: Shiki#

Syntax highlighting is a large burden for the user's browser in two ways: processing and bundle size.

  1. Processing - running a tokenizer for syntax highlighting is a lot of effort for the CPU. It just doesn't feel right to do that work every time a user views the page (or worse, every time the page changes if Html.Lazy isn't being used)
  2. Bundle size - I want to be able to use any language I want in my site. Not just JSON or CSS or Elm, but maybe I want to show an F# example to show a language feature there. Even with splitting out different language highlighting into bundles that I can import individually (which is a burden to manage as a developer), I don't want to increase the bundle size every time I refer to a new language

It's not great for developers, either. I see a real problem with sustainability. It's not sustainable to re-invent these tools for every ecosytem. There's a great NPM package called Shiki that can do this for us, which leverages code that VS Code uses for syntax highlighting. I want to perform this task using existing tools, without having to reinvent the wheel or burden the user's browser.

The solution - do the work at build-time#

Performing this work in the build step, we don't have to worry about how large the bundle size is because we're only using the dependency at build-time.

I have Shiki.elm module that I use to decode the data from Shiki. To run shiki, we can use DataSource.Port like this.

npm install --save-dev shiki
const shiki = require("shiki");

module.exports = {
  highlight: async function (fromElm) {
    const highlighter = shiki.getHighlighter({
      theme: "dark-plus",
    });
    return await highlighter.then((highlighter) => {
      return {
        tokens: highlighter.codeToThemedTokens(
          fromElm.body,
          fromElm.language,
          highlighter.getTheme(),
          {
            includeExplanation: false,
          }
        ),
        bg: highlighter.getTheme().bg,
        fg: highlighter.getTheme().fg,
      };
    });
  },
};

We can offload all of this complexity to the best tool for the job, and all without worrying about the cost of pulling anything in to our JS bundle.

I've found that this tends to work best when you can turn things into nice data, and then do dumb presentation of that data in the view. That's a good rule of thumb to make sure you're doing minimal work in the client.