Slow jekyll build time? How to improve?

I have a site with around 500 md files. The .md files are about 21k lines in total.

This takes abou 30 seconds to run a jekyll build.

This is on a modern physical server with 8 core Xeon CPU and NVMe disks.

Is this normal?

In the folder with the 500 md files, there are also some images (around 120-130 images), but I would not expect jekyll to try to parse those?

Can I do anything to speed this up?

This is my _config.yml

title: SITE
host: 0.0.0.0
description: DESC
baseurl:
url: "https://example.com"
production_url: "https://example.com"

markdown: kramdown
permalink: pretty

include: ['_pages', '_recipes']

exclude:
  - Makefile
  - README.md

collections:
  recipes:
    output: true
    permalink: /recipes/:path/index.html
  pages:
    output: true

defaults:
  - scope:
      path: "_recipes/*"
    values:
      layout: "recipe"

keep_files: ['assets']

That performance seems normal (really better than normal) – testing on a recent laptop performs at about half that speed. Jekyll is single-threaded, so all files are processed one at a time.

Have you tried incremental builds?

About static files: since Jekyll is just copying them, the performance is limited by how fast the file-system can copy. Most OSes now come with “Copy-on-Write” file-systems which makes all copying operations near-instantaneous (no matter what the file size).

Filesystem is XFS. Not sure if it does COW automatically.

I thought builds were incremental by default?

Anyway, I just tried:

Incremental:

jekyll b --incremental  22,90s user 0,23s system 99% cpu 23,158 total

Non-incremental:

jekyll b  22,76s user 0,25s system 99% cpu 23,037 total

There does not seem to be any difference.

I was hoping I could set up a cron job to rebuild this site every minute or so. But since it is using 100 % CPU for 23 seconds, that will not work…

I guess I will just have to update every hour or so…

This is quite a big site. Why would you need to rebuild it every minute?

Note that incremental regeneration is still an experimental feature. Maybe this answer will help you somehow.

Thanks.

The reason that I want to rebuild every minute or so is that there are many users of this site who might update some of the files as they use it. They would then have to wait for a rebuild to see the change - or for other users to be able to see their change… Of course not 500 files are changed on every build. Typically 1-2 files are. But most of the time there will be no changes.

I tried --profile

Not sure what to do with it though because the site render stats just lists filenames which all takes sub second time. The build process summary mentions that render and cleanup phases take about 13-14 seconds each… So this is the big part of the build time. Not sure if possible to do anything about render time. And I dont know what cleanup is?

    Build Process Summary:

    | PHASE      |    TIME |
    +------------+---------+
    | RESET      |  0.0000 |
    | READ       |  3.6402 |
    | GENERATE   |  0.0000 |
    | RENDER     | 13.7422 |
    | CLEANUP    | 13.1993 |
    | WRITE      |  0.7412 |
    | TOTAL TIME | 31.3229 |


    Site Render Stats:

    | Filename                   | Count |     Bytes |  Time |
    +----------------------------+-------+-----------+-------+
    | _includes/recipe-list.html |     5 |  2777.78K | 0.437 |
    | _pages/recipes.md          |     2 |  1111.15K | 0.186 |
    | _recipes-test2/index.md    |     1 |   555.59K | 0.084 |
    | recipes.md                 |     1 |   555.63K | 0.084 |
    | _recipes-test/index.md     |     1 |   555.59K | 0.083 |
    | _layouts/recipe.html       |   955 |  3250.79K | 0.060 |
    | _layouts/default.html      |   959 |  5605.37K | 0.034 |
    | _includes/debug.html       |   959 |   135.45K | 0.011 |
    | sitemap.xml                |     1 |     0.30K | 0.000 |
    | _pages/index.md            |     2 |     3.62K | 0.000 |
    | TOTAL (for 10 files)       |  2886 | 14551.27K | 0.980 |

                        done in 31.345 seconds.

I noticed that both recipe.html and default.html are rendered 950+ times. This is about double the amount of pages.

So something seems to cause those to be rendered twice. Any way to figure out why?

Any low hanging optimizations I can do?

I have already removed the inclusion of debug.html. That gave like a second speedup.

default.html:

    $ cat _layouts/default.html                                                  [R0 J0 L: U: ttys000 6998H]
    ---
    ---
    <!doctype html>
    <html lang="da">
        <head>
            <!-- Required meta tags -->
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <link rel="stylesheet" href="/assets/bootstrap-5.2.0-dist/css/bootstrap.min.css">
            <title>{{ page.title }} - {{ site.data.vars.site_title }}</title>
            <link rel="stylesheet" href="/assets/css/styles.css">
            <script type="module" src="{{ '/assets/js/main.js' | prepend: site.baseurl }}"></script>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
        </head>
        <body>
            <div class="container content">
                {{ content }}
            <footer class="text-muted text-center">
                &copy; Copyright 2007-2025
            </footer>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous"></script>

        </body>
    </html>

and recipe.html

    $ cat _layouts/recipe.html                                                   [R0 J0 L: U: ttys000 6999H]
    ---
    layout: default
    ---
    <div class="nav">
      <a href="/">Forsiden</a>
      &nbsp;
      →
      &nbsp;
      <a href="/recipes/">Opskrifter</a>
      &nbsp;
      →
      &nbsp;
      <a href="{{ page.url }}">{{ page.title }}</a>
    </div>
    <div class="content">
      {{ content }}
    </div>
    <hr>
    <div class="text-muted">
      Opdateret
      {% last_modified {{ page.path }} %}

    </div>
    {% if page.tags %}
    <div class="tags">
      Tags:
      {% assign tags_sorted = page.tags | sort_natural %}
      {% for tag in tags_sorted %}
      {% if forloop.last == false %}
      <a href="/tags/#tag-{{ tag }}">{{ tag }}</a>,
      {% else %}
      <a href="/tags/#tag-{{ tag }}">{{ tag }}</a>
      {% endif %}
      {% endfor %}
    </div>
    {% endif %}
    {% if page.images %}
    <hr>
    <h2>Billeder</h2>
    <div>
        {% assign images_sorted = page.images | sort_natural %}
        {% for image in images_sorted %}
        <img alt="{{ image }}" src="{{ site.baseurl }}/recipe-images/{{ image }}" class="recipe-image">
          {{ image }}
        {% endfor %}
    </div>
    {% endif %}

I did some cleanup and it improved quite a lot:

    Build Process Summary:

    | PHASE      |    TIME |
    +------------+---------+
    | RESET      |  0.0000 |
    | READ       |  3.4255 |
    | GENERATE   |  0.0000 |
    | RENDER     |  3.8325 |
    | CLEANUP    |  6.5057 |
    | WRITE      |  0.1927 |
    | TOTAL TIME | 13.9564 |


    Site Render Stats:

    | Filename                   | Count |    Bytes |  Time |
    +----------------------------+-------+----------+-------+
    | _includes/recipe-list.html |     3 | 1666.67K | 0.250 |
    | _pages/recipes.md          |     2 | 1111.15K | 0.167 |
    | recipes.md                 |     1 |  555.63K | 0.084 |
    | _layouts/recipe.html       |   953 | 2138.88K | 0.051 |
    | _layouts/default.html      |   957 | 4347.30K | 0.018 |
    | sitemap.xml                |     1 |    0.30K | 0.000 |
    | _pages/index.md            |     2 |    3.62K | 0.000 |
    | TOTAL (for 7 files)        |  1919 | 9823.54K | 0.570 |

                        done in 13.96 seconds.

recipe.html and default.html layouts are unchanged. So still interested in hearing about potential improvements.

What exactly did you do? Maybe some other users might benefit from your improvements.

But that is exactly how Jekyll works. Also, why would you want to rebuild the site if no changes were made? This is just wasting resources. I think it would be better to create some CI/CD solution to trigger the build when a file is changed, and that is quite easy to do. If the user is running the site locally, it is best to use jekyll serve instead of build.

What exactly did you do?

I removed pages that were not needed.

why would you want to rebuild the site if no changes were made?

I dont. So I guess I should instead have a cron job that first checks the VCS is there are any new changes. And only if there are new changes does it rebuild using jekyll.

I have a few other jekyll sites that are somewhat similar but much much smaller. Those I simply have a cron job that updates the VCS and then runs jekyll build. But those only takes a couple of seconds.

Users are not running site locally. They commit pages from various other systems.

Not necessarily a cron job, but it depends on your setup. All major code platforms and site hosting have support for CI/CD jobs.

It is my own bare metal server.

I think setting up jenkins/etc for this is a bit overkill

An alternative to a per-minute cron job might be to continuously run jekyll build with the --watch option, so it watches the source files and rebuilds when needed.

About incremental-builds, I think the benefit doesn’t start until the 2nd build. The first incremental build is a full build, but Jekyll records a dependency graph. After that, Jekyll uses the dependency graph to only rebuild what is needed. Of course, as @george-gca pointed out, it is still experimental, so careful testing is needed to see if it will work for a particular setup.

Don’t you use any git server like GitHub or Gitlab? Both support CI/CD. If not, your solution should be with jekyll build --watch as pointed by @chuckhoupt. Maybe you could also add --force_polling? For more options, see the docs.

Using globbed-paths (containing /* or /**) in front-matter defaults can have negative effect on build times on some OSs. The equivalent directive (in your specific use-case) is to use the key type:

defaults:
  - scope:
      type: recipes
    values:
      layout: recipe

Disclaimer: The above optimization may or may not yield noticeable gains. But is a better alternative regardless.

The huge time taken during the CLEANUP phase could be due to the large number of static files in your site. Jekyll 4 hasn’t really focused on optimizing that phase since it was never brought up as an issue. I will look into it and see if anything can be done.

2 Likes

Wow!
I just made this change:

 defaults:
   - scope:
-      path: "_recipes/*"
+      type: recipes
     values:
       layout: "recipe"

The result is astoundingly fast:

    Build Process Summary:

    | PHASE      |   TIME |
    +------------+--------+
    | RESET      | 0.0001 |
    | READ       | 0.1649 |
    | GENERATE   | 0.0000 |
    | RENDER     | 0.7622 |
    | CLEANUP    | 0.0964 |
    | WRITE      | 0.2197 |
    | TOTAL TIME | 1.2433 |


    Site Render Stats:

    | Filename                   | Count |    Bytes |  Time |
    +----------------------------+-------+----------+-------+
    | _includes/recipe-list.html |     3 | 1666.67K | 0.251 |
    | _pages/recipes.md          |     2 | 1111.15K | 0.169 |
    | recipes.md                 |     1 |  555.63K | 0.083 |
    | _layouts/recipe.html       |   477 | 1363.30K | 0.036 |
    | _layouts/default.html      |   481 | 3032.50K | 0.009 |
    | sitemap.xml                |     1 |    0.30K | 0.000 |
    | _pages/index.md            |     2 |    3.62K | 0.000 |
    | TOTAL (for 7 files)        |   967 | 7733.17K | 0.549 |

                        done in 1.253 seconds.

Thank you!