Speeding Up Slow Jekyll Builds
Jekyll has a reputation for slow builds, and for large sites it is partly deserved: Ruby and Liquid are slower than Go templates or a compiled JavaScript pipeline. But most slow Jekyll sites are not slow because of Jekyll. They are slow because of a handful of template patterns — a sidebar that loops over every post on every page, a "related posts" include that compares every post with every other, a plugin that rewrites every page after rendering — whose cost grows with the square of the site's size. Those are fixable without leaving Jekyll.
This guide profiles a 3,200-post Jekyll blog and documentation site that took 6 minutes 40 seconds to build, fixes the four hot spots, and gets it to under two minutes. It is part of Jekyll Plugin Ecosystem; if the fixes are not enough, Replacing Jekyll Plugins When Migrating to Eleventy covers the way out.
Prerequisites
- Jekyll 4.x (Jekyll 4 added caching that 3.x lacks — upgrading alone often halves build time; see Upgrading Jekyll and Ruby Versions Safely).
- Bundler and a
Gemfile.lockso every run uses the same gem versions. - A way to diff build output before and after, to make sure optimisations change nothing readers see.
Step 1: Profile
bundle exec jekyll build --profile | head -25
| Filename | Count | Bytes | Time |
|-----------------------------------+-------+----------+---------|
| _includes/related.html | 3200 | 9.12MB | 171.402 |
| _includes/sidebar-archive.html | 3406 | 41.33MB | 98.119 |
| _layouts/default.html | 3406 | 212.4MB | 41.866 |
| _includes/tag-cloud.html | 3406 | 7.81MB | 38.205 |
| _layouts/post.html | 3200 | 118.7MB | 11.940 |
| feed.xml | 1 | 2.44MB | 4.117 |
Two includes — related posts and the archive sidebar — accounted for 270 of 400 seconds. Neither did anything unusual; both looped over site.posts on every page.
Step 2: Fix Quadratic Includes
Related posts. The include compared every post's tags with every other post's tags on every post page — about 10 million comparisons. Jekyll's built-in site.related_posts (with lsi: false) is cheap but crude; the better fix computes related posts once in a small generator plugin and stores the result on each post:
# _plugins/related.rb
Jekyll::Hooks.register :site, :pre_render do |site|
by_tag = Hash.new { |h, k| h[k] = [] }
site.posts.docs.each { |p| p.data['tags'].to_a.each { |t| by_tag[t] << p } }
site.posts.docs.each do |post|
scores = Hash.new(0)
post.data['tags'].to_a.each { |t| by_tag[t].each { |o| scores[o] += 1 unless o == post } }
post.data['related'] = scores.sort_by { |o, s| [-s, -o.date.to_i] }.first(4).map(&:first)
end
end
The include then renders page.related directly. An index by tag turns the quadratic comparison into roughly linear work: 171 s became 2.3 s.
Archive sidebar. Every page rendered a month-by-month archive of all 3,200 posts — the same 12 KB of HTML each time. Rendering it once with jekyll-include-cache ({% include_cached sidebar-archive.html %}) made it render once per build: 98 s became 0.4 s.
Tag cloud. Same pattern, same fix with include_cached: 38 s became 0.2 s.
Checking the Plugins Too
The profile table covers templates, but plugin generators and hooks run outside it. Time them by building with each plugin disabled in turn (comment it out of _config.yml and the Gemfile), or wrap hook bodies in Benchmark.realtime and log the result. On this site jekyll-seo-tag, jekyll-sitemap and jekyll-feed together cost under three seconds — cheap for what they do — while a custom "reading time" plugin that re-parsed each post's HTML with Nokogiri cost 14 seconds. Computing reading time from the word count of the raw Markdown instead, which Jekyll already has in memory, reduced it to under half a second with results within one minute of the old values on every post.
Step 3: Cache Across Builds and Trim Work
- Jekyll 4's cache (
.jekyll-cache/) stores rendered Markdown and Sass; persist it between CI runs, as with the Hugo cache in Caching Hugo Builds in GitHub Actions. - Exclude what does not need processing:
exclude:fornode_modules,vendor, source images and drafts. The site was processing 1,900 files in anassets/srcdirectory that were never output. - Move asset pipelines out of Jekyll: Sass through
jekyll-sass-converteris fine, but JavaScript bundling and image resizing belong in a separate step beforejekyll build. - Use
--incrementalfor writers, not deploys: locally,jekyll serve --incrementalrebuilt an edited post in 4 seconds; for production it can miss pages that list other pages, so deploys stay full builds.
Measured Impact
GitHub Actions ubuntu-latest, Ruby 3.3, Jekyll 4.3, median of three runs, output diffed against the original to confirm it was byte-identical apart from the related-posts ordering (which changed from arbitrary to score-sorted, deliberately).
| Change | Build time |
|---|---|
| Baseline | 400 s |
| Related posts via indexed hook | 231 s |
Archive sidebar + tag cloud via include_cached | 94 s |
| Exclude unused source dirs | 83 s |
Persist .jekyll-cache in CI (warm) | 61 s |
| Local incremental rebuild, one post edited | 4 s |
Keeping It Fast
Build time crept from 90 seconds to 400 over three years on this site without anyone deciding to make it slow; each include was reasonable when written. Two habits keep it from happening again. First, run jekyll build --profile in CI on the main branch and store the top ten rows as a build artifact, alerting when total time grows more than 20% over the previous month. Second, review new includes for loops over site.posts, site.pages or collections, and ask whether the result depends on the current page. If it does not, it belongs in include_cached; if it does but only through a small key (tag, category, year), it belongs in a hook that computes it once, like the related-posts index above.
GitHub Pages users should note that jekyll-include-cache and custom plugins are not on the Pages allow-list; building in GitHub Actions and deploying the output, as in Deploying to GitHub Pages with Actions, removes that restriction and was a precondition for every fix here.
When Jekyll Is Still Too Slow
After these fixes, the remaining 61 seconds were mostly Markdown rendering and Liquid execution for 3,400 pages — genuine per-page work. A few more percent is available from kramdown options (disabling unused extensions) or switching the Markdown engine to CommonMark via jekyll-commonmark, which rendered about 30% faster here. Beyond that, Jekyll's per-page cost is simply higher than Hugo's or Eleventy's; the same content built in 9 seconds with Hugo after a migration test. If builds must stay under ten seconds for previews, that is when a migration is justified, and Eleventy vs Jekyll for Markdown-Heavy Blogs compares the most common destination.
Pitfalls & Rollback
- Optimising without profiling. Guessing usually targets the wrong template;
--profiletakes one build to answer. include_cachedon page-dependent includes. It caches by include parameters only; an include that readspage.urlreturns the first page's output everywhere. Pass page-specific values as parameters or do not cache.- Trusting
--incrementalfor deploys. It can miss listing pages; keep production builds full. - Skipping the output diff. Always compare built output before and after to catch caching mistakes.
- Rollback: every change is a template or config edit; reverting one restores its behaviour on the next build.
Conclusion
Slow Jekyll builds are usually quadratic templates, not a slow generator. Profiling a 3,200-post site found two includes looping over every post on every page; indexing related posts in a hook and caching the invariant includes took the build from 400 to 94 seconds, and exclusions plus a persisted cache brought it to 61. Fix the templates first; migrate only if per-page cost is still the bottleneck.
FAQ
How do I find out why a Jekyll build is slow?
Run jekyll build --profile. It prints a table of every template and include with the number of times it was rendered and the total time spent, which usually shows one or two includes or layouts responsible for most of the build.
Does jekyll build --incremental work in production?
It is marked experimental and can miss dependencies, such as a page that lists other pages. Use it for local writing and previews, and run full builds for production deploys.
Which plugins slow Jekyll down most?
Plugins that touch every page on every build: related-posts calculations, full-site search index generators, SEO and sitemap plugins with expensive per-page logic, and image processing without a cache. jekyll-seo-tag and jekyll-sitemap are cheap; custom generators often are not.
Is it worth migrating away from Jekyll for build speed?
Sometimes, but first fix the templates. Most slow Jekyll sites are slow because of a few Liquid loops over site.pages or site.posts on every page, which are fixable in an afternoon and often cut build time by more than half.
Related
- Parent: Jekyll Plugin Ecosystem — plugins, their costs and alternatives.
- Upgrading Jekyll and Ruby Versions Safely — Jekyll 4's caching needs an upgrade first.
- Running Jekyll on GitHub Pages Without Plugins — when plugins are not an option.
- Hugo partialCached for Faster Builds — the same idea in Hugo.
- Migrating a Docs Site from Jekyll to Hugo — the migration when fixes are not enough.