How I reduced my Jekyll build time by 61%

How I reduced my Jekyll build time by 61%

At the time of writing, Jekyll’s performance is still actively being worked on by the Core Team for an upcoming version 4 release. One of the main complaints about Jekyll for users is often the build times of larger websites. I want to take this opportunity to see just how much I can expect to improve my current site’s build times by using the latest master branch.

Sound good? Let’s get started.

Benchmarking your Jekyll website

First, we need to evaluate the current build status. We can achieve this by using Jekyll’s built-in Liquid profiler flag to spot any performance improvement opportunities.

bundle exec jekyll build --profile

Output:

Filename                         | Count |    Bytes |  Time
---------------------------------+-------+----------+------

_layouts/post.html               |   277 | 3978.62K | 3.137
_layouts/default.html            |   308 | 5580.10K | 2.943
_layouts/archive.html            |    88 | 1629.61K | 1.732
_includes/head.html              |   396 |  918.30K | 1.627
_includes/read_time.html         |   328 |   16.95K | 1.010
_includes/header.html            |   305 |  261.02K | 0.910
_includes/post_meta.html         |   277 |  214.22K | 0.726
_includes/share_icons.html       |   277 | 1021.17K | 0.671
_includes/figure                 |    72 |  112.82K | 0.659
_includes/category_tag_list.html |   277 |  150.49K | 0.537
_includes/footer.html            |   396 |  328.32K | 0.518
_includes/navigation.html        |   305 |  255.23K | 0.442
_pages/archive.md                |     1 |  219.33K | 0.377
_includes/author_bio.html        |   277 |  207.05K | 0.363
_includes/disqus_comments.html   |   277 |  155.65K | 0.349
sitemap.xml                      |     1 |   42.99K | 0.218
feed.xml                         |     1 |   58.57K | 0.038

...

done in 16.114 seconds.

As you can see above, Jekyll 3.8.5 manages to build my website which has 277 posts and 30 pages in 16.114 seconds.

Not bad, but we can definitely improve this.

⌛️ 16.114 seconds

1. Switch to the latest Jekyll master branch

Before we continue, it’s important to note that this is an unreleased development version of Jekyll and should be used with caution on a production site.

To use the latest Jekyll master branch, update your Gemfile with the following line.

- gem "jekyll", "3.8.5"
+ gem "jekyll", github: "jekyll/jekyll"

group :jekyll_plugins do
  gem "jekyll-archives"
  gem "jekyll-feed"
  gem "jekyll-remote-theme"
  gem "jekyll-seo-tag"
  gem "jekyll-sitemap"
end

Commit URL: b572c61

Jekyll 4 has a new cache API so we need to exclude .jekyll-cache in our _config.yml file for now using the exclude: option like so;

exclude:

  - .jekyll-cache

We can also add .jekyll-cache to our .gitignore file.

Next, update Jekyll with bundle update and then once again run a new build command with the Liquid --profile flag.

Jekyll builds my site in 8.55 seconds, which is already a decrease of 46.9%. We are off to a good start!

⌛️ 8.55 Seconds

2. Use Shopify’s Liquid-C gem

We will take advantage of the Shopify liquid-c gem written in C to speed up Liquid parsing. For this to work we simply need to update our Gemfile.

gem "jekyll", github: "jekyll/jekyll" 
+ gem "liquid-c"

group :jekyll_plugins do
  gem "jekyll-archives" 
  gem "jekyll-feed"
  gem "jekyll-remote-theme"
  gem "jekyll-seo-tag"
  gem "jekyll-sitemap"
end

Commit URL: 977fd24

Jekyll now builds my site in 7.74 seconds, which is a further decrease of 9.47%.

⌛️ 7.74 Seconds

3. Use Jekyll CommonMark gem

We will use jekyll-commonmark because it is faster than Kramdown. After switching, I noticed that CommonMark doesn’t support block attributes such as {:.btn .btn–primary}, because it broke parts of my website. You will need to make use of HTML tags instead.

First, update your Gemfile.

gem "jekyll", github: "jekyll/jekyll"
gem "liquid-c"

group :jekyll_plugins do
  gem "jekyll-archives" 
+  gem "jekyll-commonmark"
  gem "jekyll-feed"
  gem "jekyll-remote-theme"
  gem "jekyll-seo-tag"
  gem "jekyll-sitemap"
end

Then, add the gem to your _config.yml file under plugins.

plugins:
  - jekyll-archives
+  - jekyll-commonmark
  - jekyll-feed
  - jekyll-remote-theme
  - jekyll-seo-tag
  - jekyll-sitemap

Also, tell Jekyll to use the gem.

- markdown: kramdown
- kramdown:
-     input: GFM
-     hard_wrap: false
-     syntax_highlighter: rouge

+ markdown: CommonMark
+ commonmark:
+   options: ["SMART", "FOOTNOTES"]
+   extensions: ["strikethrough", "autolink", "table"]

Commit URL: bbb1e15

Jekyll builds my site in 7.209 seconds, which is a further decrease of 6.86%.

⌛️ 7.209 Seconds

4. Use Jekyll Include Cache plugin

To see how Jekyll _includes affect site build times, we need to take another look at the build profile above. Right away you can see some include files are being called hundreds of times, creating a performance bottleneck.

head.html called 397 times.

analytics.html called 397 times.

read_time.html called 2163 times.

navigation.html called 305 times.

Yikes! So much for being neat and tidy.

Your first move should always be to remove any _includes files which are needlessly being called upon and integrate them directly in that specific layout file where possible. We can make use of Liquid comments instead to keep things readable. Jekyll turns anything you put between {% comment %} and {% endcomment %} Liquid tags into a comment. This basically means any text within the opening and closing comment blocks will not be output, and any Liquid code within will not be executed by Jekyll.

To give you a few examples,

I removed my analytics.html include file in favour of Google Tag Manager which I placed directly in the head.html file. This will keep my build times down when I specify a production environment JEKYLL_ENV=production with my build command. fa737c2

After some thought, I decided to remove the head.html include file and directly placed all the <head> elements within the default.html layout saving me an execution time of 1.627 seconds. c8acbb3

To make it easier for users to try out a theme, Jekyll 4 will load the _config.yml file from within the current theme-gem. Knowing this, I started by moving the Disqus shortname setting to my themes _config file. 36e8f6f.

I then proceeded to move the rest of the default theme settings from my theme.yml data file to my themes _config.yml file so that Jekyll can load a fully working theme demo on install. e601721

The blog.html layout created a bottleneck during build time because it lists all 277 posts. By limiting this to the latest 10 posts and archiving older posts with just a date and title I reduced the amount of includes being generated at build time. 67840bb

Done tidying up your Jekyll Includes?

We can now make use of jekyll-include-cache, a Jekyll plugin created by Ben Balter that will cache the rendering of our Liquid includes.

If you have a computationally expensive include (such as a sidebar or navigation), Jekyll Include Cache renders the include once, and then reuses the output any time that includes is called with the same arguments, potentially speeding up your site’s build significantly.

Update your Gemfile.

gem "jekyll", github: "jekyll/jekyll" 
gem "liquid-c"

group :jekyll_plugins do
  gem "jekyll-archives"
  gem "jekyll-commonmark"
  gem "jekyll-feed"
+  gem "jekyll-include-cache"
  gem "jekyll-remote-theme"
  gem "jekyll-seo-tag"
  gem "jekyll-sitemap"
end

Update the plugins in your sites _config.yml file.

plugins:
  - jekyll-archives
  - jekyll-commonmark
  - jekyll-feed
+  - jekyll-include-cache
  - jekyll-remote-theme
  - jekyll-seo-tag
  - jekyll-sitemap

Commit URL: 264db43

It’s important to note that when using the jekyll-include-cache plugin, you cannot rely on the page context to pass variables to your include. This limitation made it impossible to cache some of my include files without breaking the themes functionality.

I started by caching the navigation in the header and footer of my site. These includes are being called on every page and don’t change that frequently.

To cache the header navigation I had to remove the page context from the navigation.html file and make a few changes to the themes header. e097f2b

To cache the footer I simply replaced {% include footer.html %} within my default.html template with {% include_cached footer.html %}. 4b86f63

If you need to clear the Jekyll cache at any time, use the following command.

bundle exec jekyll clean

After all these changes, Jekyll now builds my site in 6.262 seconds, a further decrease of 13.136%.

⌛️ 6.262 Seconds

Conclusion

I have managed to shave a grand total of 9.852 seconds off my original build time of 16.114 seconds, which is a whopping 61.139% speed improvement!

https://res.cloudinary.com/forestry-demo/image/fetch/c_limit,dpr_auto,f_auto,q_80,w_640/https://forestry.io/uploads/2019/02/jekyll-build-graph.png

It’s worth mentioning we can also enable incremental regeneration, which helps shorten Jekyll build times by only generating the documents and pages that were updated since the previous build.

bundle exec jekyll serve --incremental

Alternatively, you can set incremental: true in Jekyll’s _config.yml file. Im seeing build times as low as 5.11 seconds with this feature enabled.

Please consider posting your benchmarking results on the Jekyll Forum. Jekyll’s core team of volunteers are actively working on improving Jekyll’s performance and would appreciate your feedback.

Happy Jekylling!