Improving Jekyll Build Time

When I migrated all of my old posts to Jekyll, my build time skyrocketed. The average time was 39 seconds for 853 posts. Abysmal. This is especially annoying when I’m actively working on the site with jekyll serve and waiting 40 seconds for changes to reflect.

The first step to figuring out what takes so long is running the Liquid profiler: $ jekyll build --profile

This is what it output for me:

Filename | Count | Bytes | Time --------------------------------------------------------+-------+----------+------ _layouts/default.html | 853 | 9652.42K | 7.462 search/feed.json | 1 | 1394.16K | 4.148 _includes/header.html | 853 | 1230.35K | 3.485 _layouts/post.html | 755 | 5119.00K | 2.301 _includes/opengraph.html | 853 | 870.67K | 1.694 _includes/icons.html | 810 | 1752.89K | 1.454 sitemap.xml | 1 | 203.34K | 1.321 _includes/footer.html | 853 | 109.12K | 0.273 til/posts.yml | 1 | 14.27K | 0.147 about.md | 1 | 18.02K | 0.085 _posts/2016-07-04-posts-heatmap-calendar.md | 1 | 16.78K | 0.080 isotope.html | 1 | 389.52K | 0.048 _layouts/book.html | 34 | 198.47K | 0.039 til.md | 1 | 58.76K | 0.023 feed.xml | 1 | 139.11K | 0.022 ... with a dozen more tiny blog index pages from paginator done in 39.359 seconds.

I tried a bunch of random stuff that made tiny improvements before I started doing research: Removing extraneous files, cleaning up if blocks that I no longer used, hardcoding things in the header that relied on variables from _config.yml, etc. None of this made significant improvements. I had already moved extraneous plugins long ago and I keep Jekyll up to date, so there was nothing more to do there.

I eventually came across this post by Mike Neumegen, which mentioned jekyll-include-cache, which caches the files you tell it to with the first instance and then serves the cached versions up for subsequent requests. For includes that don’t change based on the page content, you can replace {% include example.html %} with {% include_cached example.html %}. When I did this with my sidebar, nav bar, and footer, it cut my average first build time to 28 seconds, with subsequent regeneration times around 19 seconds. Awesome! (Neumegen’s post, which mentioned jekyll-include-cache contains a lot of other helpful advice for Jekyll build optimization. Check it out!)

My next step was to look at other includes to see if I could shave more time off. Some files had a mix of static and dynamic assets, so I split out all of the static assets into more include_cached includes. This cut another 5-6 seconds off of my build time. Boom.

Now that my layouts and my includes were as optimized as I could get them without major rewrites, I turned to the next major time hog: search/feed.json. 4 seconds for a single file! This file loops through everything on the site and dumps it into a single file so that I can crawl it with Javascript to power search on my site.

I used Mat Hayward’s search project with a few tweaks. As I dug deeper on the specific functions in the feed.json generator, I noticed that everything was being converted from Markdown to HTML first, then HTML stripped out, then turned into JSON. I wondered if skipping the markdown conversion step would lead to inaccurate search, so I tried it. Not only did my search function the same, but it dropped feed.json’s build time down to around a sixth of a second.

My build time now looks like this:

Filename | Count | Bytes | Time --------------------------------------------------------+-------+----------+------ _layouts/default.html | 853 | 9564.71K | 2.922 _includes/head.html | 853 | 1571.79K | 2.623 _includes/opengraph.html | 853 | 870.67K | 2.171 sitemap.xml | 1 | 203.34K | 1.177 _layouts/post.html | 755 | 5195.80K | 0.509 search/feed.json | 1 | 1394.65K | 0.176 _posts/2016-07-04-posts-heatmap-calendar.md | 1 | 16.78K | 0.091 about.md | 1 | 18.02K | 0.076 isotope.html | 1 | 389.52K | 0.050 _layouts/book.html | 34 | 198.47K | 0.030 _includes/header.html | 1 | 1.43K | 0.021 til.md | 1 | 58.76K | 0.019 feed.xml | 1 | 139.24K | 0.018 ... with a dozen more tiny blog index pages from paginator done in 19.594 seconds.

Regeneration times when running jekyll serve are hovering around 13 seconds now, which is a significant quality of life improvement for me when managing this blog.

There is probably more work I can do with moving the highlighter to the front end with JS and simplifying the Liquid in head.html and opengraph.html, but I’m stopping here for now.