Posts tagged with "Hugo"

fun with static site generators

Table of Contents

  • Introduction
  • Test Environment
    • Hardware
    • Test Data
  • Hugo
  • Eleventy
  • BSSG
  • Zola
  • Nikola
  • Pelican
  • Jekyll
  • Useful SSG performance resources

Introduction

Static site generators (SSG's) are often used for blogs. SSG's typically process Markdown files into static HTML files. The use of static HTML files (rather than dynamically generating the site using a database) offers benefits for performance and security.

I use Hugo for my blog and have used (or experimented with) the following static site generators (SSG's).

  • Hugo (Go)
  • Eleventy (Javascript)
  • BSSG (Bash)
  • Zola (Rust)
  • Nikola (Python)
  • Pelican (Python)
  • Jekyll (Ruby)

A throwaway post from Neil Brown on Mastodon prompted me to investigate the build performance of different static site generators with a significant volume of posts (1,000).

Neil reduced the build time for his Hugo blog simply by adding more hardware resources. A decent strategy involving minimal effort producing an immediate improvement.

'I have upped the RAM in the VM running my Web server to a heady 1GB, and added a second CPU core'.

'That has halved the Hugo build time for my blog [from 4 minutes to 2 minutes]'.

This comment surprised me as I also use Hugo which builds my entire blog, containing 1,000 pages, in less than two seconds on a 10 year old desktop computer.

Neil's blog is self-hosted and uses Hugo. A 'View Source' reveals the version of Hugo in use. Neil is using v0.131.0 (released in August 2024).

<meta name="generator" content="Hugo 0.131.0">

The latest version of Hugo is 0.151.1 (released in October 2025). However, Neil is using the Hugo version included in the Debian (Trixie) repositories.

Reviewing the 'Archives' page, Neil's blog contains 457 posts.

Neil moved his blog from HTMLy to Hugo in 2023 and was using Hugo v.0.111.3 (from the Debian repos) back then and used the Etch theme.

Looking at the Etch theme, it is clear Neil has stuck with this attractive, minimal Hugo theme since then.

Given the disparity between Neil's build time and mine, I thought it would be fun to compare the performance of Hugo with different themes as well as other popular static site generators.

Test Environment

Hardware

  • Lenovo M900 Tower Desktop (SFF)
  • CPU: Intel i7-6700 CPU @ 3.40GHz (4 cores, 8 threads)
  • Memory: 48GB
  • Disk: 1TB
  • O/S: Linux 6.17.2

Test Data

I used my personal blog as the data set for the performance tests.

  • 1028 articles (Markdown)
  • Content spanning twenty years (2005 to 2025)
  • 116 code blocks
  • 74 static images
  • 29 categories
  • 46 tags

Hugo

Hugo: https://gohugo.io/

Hugo is a popular static site generator written in Go with a reputation for speed and performance.

$ hugo version
hugo v0.151.1+extended+withdeploy linux/amd64 BuildDate=unknown
$ go version
go version go1.25.3 X:nodwarf5 linux/amd64

The tests ran the standard 'hugo build' command three times and took the average elapsed time.

Theme Time (secs)
Ananke 0.615
PaperMod (Base) 1.067
PaperMod (Custom) 1.633
Etch (Base) 1.911
Etch (Related) 1.958
BearBlog 0.425
Simple 0.377
Beautiful Hugo 1.541

Most Hugo themes used the default, out of the box settings with no customisation.

The 'PaperMod (Custom)' test used my personal blog which includes additional 'Archive', 'Categories', 'Posts' pages and search functionality.

The timings for 'Etch' surprised me as it is a relatively simple theme that displays a list of all posts.

I added support for up to 15 'Related Posts' using Neil's code but saw no noticeable increase in build time (which is less than two seconds).

Hugo supports 'Related Posts' functionality and the list of articles is built during the build (regardless of whether it is used or not).

Ananke: https://github.com/theNewDynamic/gohugo-theme-ananke

PaperMod: https://github.com/adityatelange/hugo-PaperMod

Etch: https://github.com/LukasJoswiak/etch

BearBlog: https://github.com/janraasch/hugo-bearblog

Simple: https://github.com/maolonglong/hugo-simple/

Beautiful Hugo: https://github.com/halogenica/beautifulhugo

Hugo provides useful diagnostics about potential performance bottlenecks.

Here is the template metrics report for the Etch theme (with related posts). There are three candidate templates that could be cached (header, footer and posts).

$ hugo --templateMetrics --templateMetricsHints
Template Metrics:
     cumulative       average       maximum      cache  percent  cached  total
       duration      duration      duration  potential   cached   count  count  template
     ----------      --------      --------  ---------  -------  ------  -----  --------
   2.691672237s   53.833444ms   83.570806ms          0        0       0     50  rss.xml
   1.409607939s    1.345045ms   11.568865ms          0        0       0   1048  single.html
   322.542988ms      307.77µs    2.927895ms         28        0       0   1048  _partials/related.html
   126.863653ms      115.54µs    1.183336ms         44        0       0   1098  _partials/head.html
    81.827628ms   40.913814ms   41.042982ms        100        0       0      2  _partials/posts.html
    79.788949ms      25.193µs     363.218µs          0        0       0   3167  li.html
    55.705094ms    1.160522ms    7.457174ms          0        0       0     48  _default/taxonomy.html
    44.028022ms   44.028022ms   44.028022ms          0        0       0      1  index.html
    43.649693ms   43.649693ms   43.649693ms          0        0       0      1  list.html
    32.992786ms   32.992786ms   32.992786ms          0        0       0      1  sitemap.xml
     24.19536ms      22.035µs     948.957µs        100        0       0   1098  _partials/header.html
     7.410874ms       6.749µs     143.528µs        100        0       0   1098  _partials/footer.html
      961.219µs     106.802µs     278.188µs          0        0       0      9  _shortcodes/figure.html
      708.877µs     141.775µs     299.575µs          0        0       0      5  _markup/render-table.html.html
      551.337µs     110.267µs     208.377µs          0        0       0      5  _markup/render-table.rss.xml
      105.423µs      52.711µs      88.545µs          0        0       0      2  alias.html
       15.147µs      15.147µs      15.147µs          0        0       0      1  /css/dark.css
         1.76µs        1.76µs        1.76µs          0        0       0      1  404.html

Total in 2002 ms

Eleventy

Eleventy: https://www.11ty.dev/

Eleventy is a popular SSG written in Javascript.

Eleventy Base Blog (v9): https://github.com/11ty/eleventy-base-blog

The Eleventy Base Blog theme is minimal and not dissimilar in appearance from the Hugo PaperMod theme.

$ node --version
v20.19.5
$ npx @11ty/eleventy --version
3.1.2

Building the Eleventy blog. Eleventy doesn't have separate 'build' and 'serve' commands.

The Eleventy build summary for my blog.

$ npx @11ty/eleventy --serve
[11ty/eleventy-img] 143 images optimized (143 deferred)
[11ty] Benchmark   1664ms  11%  1052× (Configuration) "@11ty/eleventy/html-transformer" Transform
[11ty] Copied 5 Wrote 1043 files in 14.53 seconds (13.9ms each, v3.1.2)
[11ty] Server at http://localhost:8080/

Eleventy supports incremental builds using the '--incremental' parameter which only processes content modified since the last build.

Initially, I saw no difference using '--incremental' but the Eleventy documentation suggested adding the '--ignore-initial' option. This reduced the build time significantly from 14 seconds to sub-second.

$ npx @11ty/eleventy --serve --incremental --ignore-initial
[11ty] Copied 5 Wrote 0 files in 0.77 seconds (v3.1.2)
[11ty] Watching…
# Add a new post with tags
[11ty/eleventy-img] 3 images optimized (3 deferred)
[11ty] Wrote 32 files (skipped 1012) in 0.51 seconds (v3.1.2)
# Add more text to existing post
[11ty/eleventy-img] 3 images optimized (3 deferred)
[11ty] Wrote 32 files (skipped 1012) in 0.46 seconds (v3.1.2)

Summary of timings for Eleventy

Theme Time (secs)
Eleventy (Full) 14.42
Eleventy (Incremental) 0.46

BSSG

BSSG - https://bssg.dragas.net/

Bash Static Site Generator (BSSG) is an SSG created by Stefano Marinelli. BSSG is written in the Bash shell

BSSG is a relatively new SSG. The first public release of BSSG was in March 2025 but there have been 14 subsequent releases.

BSSG includes a broad range of themes, support for incremental builds, parallel processing and a post article editor to manage content

BSSG doesn't currently support 'Categories' so all existing 'Categories' were migrated to 'Tags'.

It is possible this skewed the data set slightly and adversely affected performance as it resulted in four tags having a lot of associated posts. BSSG can list all tags with article counts using the 'bssg.sh tags' command.

Tag Count
blogging 236
football 112
software 122
UK 260
$ bssg.sh
BSSG - Bash Static Site Generator (v0.32)
$ bash --version
GNU bash, version 5.3.3(1)-release (x86_64-pc-linux-gnu)

Initial BSSG build from scratch.

$ bssg.sh build

BBSG uses incremental builds and only rebuilds what has changed.

Theme Time (secs) Notes
Default 2,389 Full (2,389 secs = 39 mins)
Default 23 Unchanged.
Default 61 Add new post (no tags).
Default 100 Add new post (existing tag).
Default 62 Modify existing post.
Default 107 Add existing tag to existing post.
Default 83 Add new tag to existing post.

By default, BSSG generates related posts based on the 'Tags' in each post. The default number of related posts displayed is 3. If this feature is disabled, then the build time is reduced significantly.

Theme Time (secs) Notes
Default 169 Full
Default 12 Unchanged.
Default 63 Add new post (no tags).
Default 62 Add new post (existing tag).
Default 64 Modify existing post.
Default 62 Add existing tag to existing post.
Default 70 Add new tag to existing post.

BSSG also supports parallel processing using the GNU parallel shell tool. The GNU parallel package is very lightweight (< 1MB).

BSSG detects the presence of GNU parallel automatically and spawns N processes in parallel where N is the number of threads available.

Checked dependencies. Parallel available: true
GNU parallel found! Using parallel processing.

On my computer, this resulted in BSSG spawning 8 Bash processes which may have been too many as the load average climbed to between 10 and 15.

However, the elapsed time for the initial build of a blog with 1,000 posts reduces from 39 minutes to under 10 minutes.

Before you exclaim '10 minutes when Hugo and Eleventy are sub-second', think about how often you completely rebuild every single post on your blog. Not very often.

The typical use case is writing a new blog post. There may be occasions when you change theme or spend two weeks consolidating all your tags and categories but, hopefully, those should be relatively rare.

Theme Time (secs) Notes
Default 559 Full. Parallel.
Default 32 Unchanged.
Default 60 Add new post (no tags).
Default 75 Add new post (existing tag).
Default 67 Modify existing post.
Default 73 Add existing tag to existing post.
Default 67 Add new tag to existing post.

Removing 'Related Posts' and running in parallel reduces the time for a full build to 1 minute and an incremental build to 45 seconds.

Theme Time (secs) Notes
Default 59 Full. Parallel.
Default 30 Unchanged.
Default 44 Add new post (no tags).
Default 44 Add new post (existing tag).

One big advantage of BSSG is the ability to quickly and easily change themes. You simply select a theme, modify the THEME entry in the configuration file and it just works. This is because BSSG themes use a single CSS style sheet. This may limit the functionality available but it just works.

Zola

Zola - https://www.getzola.org/

Zola is a SSG written in Rust. Like Hugo, Zola is a single executable. Like Hugo, Zola is fast. Like Hugo, changing themes in Zola is not simply a case of modifying the THEME entry in 'config.toml'. Each theme seems to have additional, custom configuration options that need to be set.

$ zola --version
zola 0.21.0
$ rustc --version
rustc 1.90.0 (1159e78c4 2025-09-14) (Arch Linux rust 1:1.90.0-3)

Serene Theme - https://github.com/isunjn/serene

Building the blog

$ zola build

The Zola build time for 1,000 posts was so lightning fast, I had to check it actually worked !

$ zola build
Building site...
Checking all internal links with anchors.
> Successfully checked 0 internal link(s) with anchors.
-> Creating 1030 pages (0 orphan) and 1 sections
Done in 351ms.

Like Hugo, Zola also has a live development server that watches for changes to the site in real-time. This is also fast.

Building site...
Checking all internal links with anchors.
> Successfully checked 0 internal link(s) with anchors.
-> Creating 1031 pages (0 orphan) and 1 sections
Done in 309ms.

Listening for changes in zola-blog/{config.toml,content,sass,static,templates,themes}

Web server is available at http://127.0.0.1:1111 (bound to 127.0.0.1:1111)

Change detected @ 2025-10-14 13:17:09
-> Content changed zola-blog/content/posts/zola-new-post.md
Checking all internal links with anchors.
> Successfully checked 0 internal link(s) with anchors.
-> Creating 1031 pages (0 orphan) and 1 sections
Done in 283ms.

Finally I experimented with a couple more themes.

Linkita - https://www.getzola.org/themes/linkita/

BearBlog - https://www.getzola.org/themes/bearblog/

PaperMod - https://www.getzola.org/themes/papermod/

Theme Time (secs) Notes
Serene 0.35 Full
Serene 0.28 Incremental
Linkita 1.77 Full
Linkita 1.70 Incremental
Bearblog 0.26 Full
Bearblog 0.25 Incremental
PaperMod 20.70 Full
PaperMod 20.51 Incremental

Nikola

Nikola - https://getnikola.com/blog/

$ python -V
Python 3.13.7
$ nikola version
Nikola v8.3.3

Useful Nikola commands.

nikola build
nikola serve --browser
nikola auto

To force a full rebuild in Nikola, you need to remove the 'output' directory.

You also need to use the Linux time command to get the elapsed timings for the 'nikola build' command.

Nikola includes the wonderful blog.txt theme (originally written for Wordpress by Scott Wallick) so kudos to Nikola's author Roberto Alsina for that.

Theme Time (secs) Notes
Default 44.86 Full
Default 4.72 Unchanged
Default 5.86 Add new post (no tags).
Default 5.75 Add new post (existing tag).
Default 5.98 Modify existing post.
Default 5.87 Add existing tag to existing post.
Default 5.70 Add new tag to existing post.
blogtxt 47.34 Full
blogtxt 4.93 Unchanged
blogtxt 4.78 Add new post (no tags).
blogtxt 4.82 Add new post (existing tag).

Pelican

Pelican - https://getpelican.com/

Create a dedicated virtual environment for Pelican.

$ workon Pelican
(Pelican) $ python -V
Python 3.13.7
(Pelican) $ pelican --version
4.11.0

Pelican doesn't have separate build and server commands. You simply run the development server which builds the site and watches for any changes.

(Pelican) $ pelican --autoreload --listen
Serving site at: http://127.0.0.1:8000 - Tap CTRL-C to stop
Done: Processed 1034 articles, 0 drafts, 0 hidden articles, 0 pages,
0 hidden pages and 0 draft pages in 3.65 seconds.

Add a new post (incremental build).

-> Modified: pelican-blog/content/my-pelican-post.md.
re-generating...
Done: Processed 1035 articles, 0 drafts, 0 hidden articles, 0 pages,
0 hidden pages and 0 draft pages in 2.96 seconds.

Summary

Theme Time (secs) Notes
Default 3.65 Full
Default 2.96 Incremental

Jekyll

Ruby based blog.

$ ruby -v
ruby 3.4.6 (2025-09-16 revision dbd83256b1) +PRISM [x86_64-linux]
$ bundle exec jekyll -v
jekyll 4.4.1

Jekyll base theme (minima) - https://github.com/jekyll/minima

Build the blog

$ bundle exec jekyll serve
<snip>
Run in verbose mode to see all warnings.
                  done in 2.788 seconds.
Auto-regeneration: enabled for '/home/andy/devel/my-jekyll-blog'
Server address: http://127.0.0.1:4000/

Live reload uses a different command and port

Run in verbose mode to see all warnings.
                    done in 4.7 seconds.
 Auto-regeneration: enabled for 'devel/my-jekyll-blog'
LiveReload address: http://127.0.0.1:35729
    Server address: http://127.0.0.1:4000/
  Server running... press ctrl-c to stop.

Jekyll also produces a lot of warnings (deprecation) that clutter up the display. This is surprising (and irritating) for the latest version of Jekyll and the standard, bundled theme.

There is a '--quiet' option for 'jekyll build' but this doesn't appear to silence the warnings.

Attempting to access the live development server on port 35729 fails. However, the live reload is actually available on port 4000.

This port only serves livereload.js over HTTP.

Given Jekyll was first released back in 2008, Jekyll feels rather neglected and outdated to me. Tags didn't work properly. All tags were processed and listed but the click through from an individual article as '404 - Not Found' error.

Also Jekyll insists on blog posts following a naming convention ('yyyy-mm-dd-title.md').

Theme Time (secs) Notes
Default 5.091 Full
Default 1.029 Incremental
Default 8.561 Live reload

Useful SSG performance resources

Zach Leatherman (Eleventy lead developer) performed some performance benchmarks (in 2022) which are a useful benchmark comparing SSG's for pure Markdown conversion throughput for large sites.

However, Zach's tests don't include meta-data (tags, categories, dates) so aren't necessarily representative of a real-life blog or site.

https://www.zachleat.com/web/build-benchmark/

Generating representative test data is difficult but this Bash script scrapes a random Wikipedia page and generates Markdown (including tags and categories).

https://gist.github.com/jgreely/2338c72c825d2a93713e4f0fc0025985

Each SSG has its own format for front-matter. There are even two different formats for front matter; TOML and YAML.

Hugo has a very useful builtin conversion function to convert the Hugo front matter an all posts between the formats (including JSON).

$ hugo convert --help
Usage:
  hugo convert [command]

Available Commands:
  toJSON      Convert front matter to JSON
  toTOML      Convert front matter to TOML
  toYAML      Convert front matter to YAML

Hugo blog now hosted on Netlify

This blog uses Hugo and was previously hosted on Amazon S3 storage. The traffic and hence the costs were minimal (zero).

After recently having to completely re-install Arch Linux after an idiotic mistake, I realised that Hugo was out of date, my Hugo theme was out of date and I'd forgotten precisely how the deployment to S3 actually worked.

I was toying with taking my ball home in a mindless fit of pique, migrating 1,000 posts to Eleventy and I also looked at the Publii static site CMS with interest.

However, that would have been foolish as I already had a Hugo blog that worked fine. The problem was I never actually used it. It's frictionless blogging but you have to actually produce content occasionally. The friction (for me at least) is typing the words in - not building, previewing and publishing the site.

So, in the great blog unification process, I resurrected all my historical blog posts, my very limited content on write.as and deposited the lot into Hugo.

I changed the theme to PaperMod as it was a modern, clean, minimal, single column theme reminiscent of write.as.

The migration was pretty straightforward as all the existing posts were already in Markdown format and the YAML front matter just needed tweaking.

As I had used Hugo and Netlify for a friend's conventional Web site, I took the opportunity to switch the blog from S3 to Netlify which gives me SSL support and is generally a 'one click' operation to push the content to GitHub and then publish on Netlify.