Reducing bundle size of Highlight.js with Webpack 2

Disclaimer: This post is based on Brian Jacobel's great post about reducing highlight.js's bundle size, since he doesn't have a comment section on his blog, I figured I would add a few more details as well as some updates for webpack2 here.

In an attempt to improve the performance of this blog, I am currently trying to bring down the size of my assets. I use webpack2 to bundle the code for my apps and I currently split my chunks into vendor (all third-pary libraries, i.e. node modules) and application code (although I might use some more advanced code splitting techniques in the future).

Bundle Analysis before, with highlight.js taking up almost 25% of the total size
The problem: highlight.js is taking up almost 25% of the total bundle size.

When I started analysing my vendors chunk, I was quite surprised to find that highlight.js took up almost 25% of my total bundle size. The problem? All available languages are automatically included, although I am only using a small sub-set and am not planning on adding more any time soon either.

In my application, I use highlight.js together with marked to highlight all code samples inside the markdown I get from the CMS:

import marked from 'marked'
import highlighter from 'highlight.js'

export const markdown = (markdownString) => {
  const highlight = (code, language) => {
    if (language) {
      try {
        return highlighter.highlight(language, code).value
      } catch (error) {
        // language not found
      }
    }
    return highlighter.highlightAuto(code).value
  }

  return marked(markdownString, { highlight })
}

Thanks to a blog post by Brian Jacobel, I quickly found out that I can actually import the highlight.js highlighter and add languages to it manually:

import highlighter from 'highlight.js/lib/highlight'
import bash from 'highlight.js/lib/languages/bash'

highlighter.registerLanguage('bash', bash)

You can find a list of all supported languages on highlight.js' website or by having a look at their source code on GitHub.

After finding the languages you want to add, you can either import and register the languages one by one, or instead dynamically require them, like Brian explains in his blog, i.e. something like this:

const languages = ['bash', 'css', 'javascript', 'json', 'xml'] // languages you need
const registerLanguage = name => {
  const lang = require(`highlight.js/lib/languages/${name}`)
  highligher.registerLanguage(name, lang)
}
languages.forEach(registerLanguage)

Thanks to using webpack2's tree shaking, this is all that has to done in order to remove all unnecessary languages from highlight.js. Please refer to the original post for details on how to achieve the same with webpack1.

Results

Before After
total bundle stats: 2.63 MB
parsed: 1.27 MB
gzipped: 400.39 KB
stats: 1.94 MB
parsed: 777.41 KB
gzipped: 218.24 KB
highlight.js stats: 750.47 KB
parsed: 539.58 KB
gzipped: 188.22 KB
stats: 39.09 KB
parsed: 17.41 KB
gzipped: 6.45 KB

Or in pictures (before on the left, after on the right): Before: 24.4%, After: 1.7% of the bundle size

Beyond highlight.js!

I guess the message of this post, despite concentrating on the specific case of highlight.js, is that knowing your third party libraries is really important. Don't add anything you don't really need and regularly analyse your bundle to find and eliminate more libraries that take up disproportionate amounts of space. In the case of this blog, I found a second library, taking up 15% of my bundle size: moment.js. And I only use moment.js to format Dates... And only in approximately 3 places! So instead of only removing their locales, as discussed in many places already, I actually just wrote my own date formatting utility.

And if you think these enhancements are not worth it... In the specific case of this blog, here some stats (from lighthouse):

Before After
First meaningful paint 7,440 ms 2,390 ms
First Interactive (beta) 14,010 ms 7,060 ms
Perceptual Speed Index 7,566 2,429
Share article