Optimizing Hugo Blog Performance: How I Made My Site 3x Faster

Learn how to dramatically speed up your Hugo blog with practical performance optimization techniques. Real metrics and step-by-step improvements.

🕒 7 min read
Hugo website performance metrics showing before and after optimization

Hey there! 👋 Remember when we set up our Hugo blogs with custom domains and fancy shortcodes? Well, today we’re tackling something that kept me up at night: website performance.

I’ll never forget the day I ran my first performance test on my Hugo blog. The results were… humbling. 🐢 My site was slower than my grandma’s dial-up internet (okay, maybe not that bad, but close!).

But here’s the good news: I managed to triple my site’s speed using simple, practical optimizations. And today, I’m sharing exactly how I did it - no complex server configurations required!

Why Performance Matters More Than You Think

Before we dive into the technical stuff, let me share why I became obsessed with performance:

The Reality Check

  • 47% of users expect pages to load in 2 seconds or less
  • 40% will abandon a site that takes more than 3 seconds to load
  • Google uses page speed as a ranking factor

But here’s what really hit home for me: I noticed my bounce rate was sky-high. People were visiting but leaving immediately. When I fixed the performance issues, my time-on-page increased by 2.5x!

My Performance Journey: Before and After

Before Optimization:

  • Page Load Time: 4.8 seconds 😬
  • Google PageSpeed Score: 42/100
  • Largest Contentful Paint: 3.9 seconds
  • Bounce Rate: 68%

After Optimization:

  • Page Load Time: 1.6 seconds 🚀
  • Google PageSpeed Score: 92/100
  • Largest Contentful Paint: 1.2 seconds
  • Bounce Rate: 32%

The best part? Most of these improvements took less than 30 minutes each!

Step 1: Image Optimization - The Low-Hanging Fruit

Images were the biggest culprit on my site. Here’s how I fixed them:

Install Hugo Pipes for Image Processing

First, add this to your config.toml:

[imaging]
resampleFilter = "CatmullRom"
quality = 85

Optimize Images in Your Templates Instead of using regular image tags:

<!-- ❌ Old way -->
<img src="/images/hero.jpg" alt="Hero image">

<!-- ✅ Optimized way -->
{{ $image := resources.Get "images/hero.jpg" }}
{{ $small := $image.Resize "800x" }}
{{ $medium := $image.Resize "1200x" }}

<picture>
  <source media="(max-width: 600px)" srcset="{{ $small.RelPermalink }}">
  <source media="(max-width: 1200px)" srcset="{{ $medium.RelPermalink }}">
  <img src="{{ $image.RelPermalink }}" 
       alt="Hero image" 
       loading="lazy"
       width="{{ $image.Width }}" 
       height="{{ $image.Height }}">
</picture>

Create an Image Shortcode I made this shortcode to automate optimized images:

{{/* layouts/shortcodes/image.html */}}
{{ $src := .Get "src" }}
{{ $alt := .Get "alt" }}
{{ $class := .Get "class" | default "" }}

{{ $image := resources.Get $src }}
{{ if $image }}
  {{ $small := $image.Resize "800x q85" }}
  {{ $medium := $image.Resize "1200x q85" }}
  
  <picture class="{{ $class }}">
    <source media="(max-width: 600px)" srcset="{{ $small.RelPermalink }}">
    <source media="(max-width: 1200px)" srcset="{{ $medium.RelPermalink }}">
    <img src="{{ $image.RelPermalink }}" 
         alt="{{ $alt }}"
         loading="lazy"
         width="{{ $image.Width }}" 
         height="{{ $image.Height }}">
  </picture>
{{ else }}
  <img src="{{ $src }}" alt="{{ $alt }}" class="{{ $class }}">
{{ end }}

Usage:

{{< image src="images/my-photo.jpg" alt="Description" class="blog-image" >}}

Step 2: CSS and JavaScript Optimization Bundle and Minify Your Assets Create this in layouts/partials/head.html:

{{/* CSS */}}
{{ $styles := resources.Get "css/main.css" }}
{{ $styles = $styles | resources.PostCSS }}
{{ if hugo.IsProduction }}
  {{ $styles = $styles | minify | fingerprint | resources.PostProcess }}
{{ end }}
<link href="{{ $styles.RelPermalink }}" rel="stylesheet" />

{{/* JS */}}
{{ $js := resources.Get "js/main.js" }}
{{ if hugo.IsProduction }}
  {{ $js = $js | minify | fingerprint }}
{{ end }}
<script src="{{ $js.RelPermalink }}" defer></script>

Critical CSS Inlining For above-the-fold content, I inline critical CSS:

{{/* Get critical CSS for above-the-fold content */}}
{{ $critical := resources.Get "css/critical.css" | minify }}
<style>{{ $critical.Content | safeCSS }}</style>

{{/* Load rest async */}}
{{ $styles := resources.Get "css/main.css" | minify }}
<link rel="preload" href="{{ $styles.RelPermalink }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ $styles.RelPermalink }}"></noscript>

Step 3: Hugo Configuration Optimizations Enable Gzip Compression Add to your config.toml:

[server]
[[server.headers]]
pattern = ".*\\.(js|css|txt|html|xml)$"
[server.headers.values]
Content-Encoding = "gzip"

# Enable brotli for even better compression
[[server.headers]]
pattern = ".*\\.(js|css|html|xml|json|svg|txt)$"
[server.headers.values]
Content-Encoding = "br"

Cache Control Headers

[[server.headers]]
pattern = ".*\\.(js|css)$"
[server.headers.values]
Cache-Control = "max-age=31536000, immutable"

[[server.headers]]
pattern = ".*\\.(png|jpg|jpeg|gif|webp|svg)$"
[server.headers.values]
Cache-Control = "max-age=2592000"

[[server.headers]]
pattern = ".*\\.(html|xml|json)$"
[server.headers.values]
Cache-Control = "max-age=3600"

Step 4: Font Optimization Fonts were another performance killer for me. Here’s my solution:

Local Font Loading

{{/* Preload critical fonts */}}
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>

<style>
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-var.woff2') format('woff2');
}

/* System font stack for initial load */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.fonts-loaded body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
</style>

<script>
// Load fonts after initial render
document.fonts.ready.then(() => {
  document.documentElement.classList.add('fonts-loaded');
});
</script>

Step 5: Lazy Loading and Progressive Enhancement Lazy Load Images and Embeds

<!-- Lazy load images -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" class="lazy">

<!-- Lazy load YouTube embeds -->
<div class="youtube-lazy" data-id="VIDEO_ID">
  <img src="thumbnail.jpg" loading="lazy" class="youtube-thumbnail">
  <button class="youtube-play">▶</button>
</div>

<script>
// Simple lazy loading
document.addEventListener('DOMContentLoaded', function() {
  const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'));
  
  if ('IntersectionObserver' in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.classList.remove('lazy');
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });
    
    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  }
});
</script>

Step 6: Build Process Optimizations Hugo Build Command Optimization I optimized my build command:

# ❌ Old way
hugo

# ✅ Optimized way
hugo --minify --gc --enableGitInfo

# For production
hugo --minify --gc --enableGitInfo --forceSyncStatic --cleanDestinationDir

Partial Caching For expensive partials, I added caching:

{{/* Only rebuild this partial when data changes */}}
{{ $cacheKey := print "recent-posts-" (now.Format "2006-01-02") }}
{{ $recentPosts := partialCached "recent-posts.html" . $cacheKey }}
{{ $recentPosts }}

Real Performance Metrics From My Blog Here are the actual improvements I saw:

Core Web Vitals Improvement Largest Contentful Paint: 3.9s → 1.2s

First Input Delay: 280ms → 45ms

Cumulative Layout Shift: 0.28 → 0.05

User Experience Impact Bounce Rate: -53%

Pages per Session: +42%

Conversion Rate: +28%

Common Performance Mistakes I Made ❌ Loading All CSS/JS on Every Page Fix: Split bundles and load conditionally

❌ Unoptimized Images Fix: Resize, compress, and use modern formats

❌ Blocking Render with Web Fonts Fix: Use font-display: swap and system fonts initially

❌ No Caching Strategy Fix: Set proper cache headers

Testing Your Performance Tools I Use Regularly:

# Google PageSpeed Insights
https://pagespeed.web.dev/

# WebPageTest
https://www.webpagetest.org/

# Hugo built-in
hugo --templateMetrics

My Testing Routine: Local: hugo server and Chrome DevTools

Staging: Netlify preview deployments

Production: Real user monitoring

Advanced Optimizations Service Worker for Caching I added a simple service worker for offline functionality:

// static/sw.js
const CACHE_NAME = 'hugo-blog-v1';
const urlsToCache = [
  '/',
  '/css/main.css',
  '/js/main.js'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

Preload Key Requests

{{/* Preload critical resources */}}
<link rel="preload" href="{{ "css/critical.css" | absURL }}" as="style">
<link rel="preload" href="{{ "js/main.js" | absURL }}" as="script">

What’s Next? Hugo Security Best Practices! In our next Web Dev post, we’ll dive into “Securing Your Hugo Blog” where we’ll cover:

Content Security Policy (CSP) headers

HTTPS and SSL configuration

Preventing common vulnerabilities

Privacy and GDPR considerations

Your Performance Mission This week, try these quick wins:

Run a PageSpeed test on your site

Optimize 3 images using the techniques above

Implement one caching header

Share in comments: What was your biggest performance improvement?

Wrapping Up Optimizing my Hugo blog’s performance was one of the most rewarding technical challenges I’ve tackled. The results spoke for themselves: happier users, better SEO, and higher engagement.

Remember: Performance optimization is a journey, not a destination. Start with the low-hanging fruit, measure your results, and keep iterating.

The techniques I shared today took my site from “meh” to “wow” - and they can do the same for yours!

What performance challenges are you facing with your Hugo site? Share your PageSpeed scores and let’s brainstorm solutions together in the comments!

Comments & Discussion

Join the conversation using your GitHub account. Comments are powered by Utterances.

ad ad