Skip to content

Static Site Optimization -- Build Speed, Asset Pipeline & Caching

DodaTech Updated 2026-06-22 7 min read

In this tutorial, you'll learn about Static Site Optimization. We cover key concepts, practical examples, and best practices.

Static site optimization focuses on reducing build times, shrinking asset sizes, and implementing caching strategies to deliver fast load times and efficient CI/CD pipelines for production websites.

What You'll Learn

Why It Matters

A slow build pipeline wastes developer time and delays deployments. An unoptimized asset pipeline serves oversized images and unminified CSS, hurting Lighthouse scores and user experience. Proper caching strategies can eliminate redundant builds and improve TTFB (Time to First Byte) for global audiences. At DodaTech, our site builds over 2,900 pages in under 2 seconds thanks to careful optimization of Hugo's build pipeline and Cloudflare's edge caching.

Real-World Use

An e-commerce site with 50,000 product pages reduces its build from 45 minutes to 4 minutes by implementing incremental builds. A documentation team cuts their CI/CD bill by 60% by caching Hugo's resources/_gen directory between builds. A marketing site improves its Lighthouse Performance score from 62 to 98 by optimizing its asset pipeline and implementing a CDN cache strategy.

Optimization Pipeline Overview

flowchart LR
  A[Source Content] --> B[Build Step]
  B --> C[Cache Check]
  C -->|Miss| D[Full Build]
  C -->|Hit| E[Incremental Build]
  D --> F[Asset Pipeline]
  E --> F
  F --> G[Minify/CSS/JS]
  G --> H[Image Optimize]
  H --> I[Fingerprint]
  I --> J[CDN Cache]
  J --> K[Edge Cache]
  K --> L[Browser Cache]
  style C fill:#f90,color:#fff

Build Speed Optimization

Hugo Cache Configuration

Hugo's build cache stores parsed templates, SCSS compilation results, and image processing outputs. Configuring the cache properly can reduce rebuild times by 80%.

# config/hugo.prod.toml -- Cache and build optimizations
baseURL = "https://tutorials.dodatech.com"

[build]
  useResourceCacheWhen = "always"
  writeStats = true

[caches]
  [caches.getcsv]
    dir = ":resourceDir/_gen"
    maxAge = "720h"  # 30 days
  [caches.assets]
    dir = ":resourceDir/_gen"
    maxAge = "720h"
  [caches.images]
    dir = ":resourceDir/_gen"
    maxAge = "720h"

[imaging]
  resampleFilter = "Box"
  quality = 82
  anchor = "Smart"

Expected behavior: Hugo caches processed assets for 30 days. On subsequent builds, unchanged assets are loaded from cache instead of being reprocessed. The useResourceCacheWhen = "always" setting ensures the cache is used on all builds, including CI.

Parallel Processing and Ignored Content

# Optimize by ignoring draft and future content in production
[build]
  disableKinds = ["sitemap", "robotsTXT"]
  useResourceCacheWhen = "always"

[module]
  [[module.mounts]]
    source = "content"
    target = "content"
    lang = "en"

Expected behavior: Disabling unused output kinds (like sitemap generation during development) reduces build work. Mounting content with specific language settings avoids processing content for all languages when only one is needed for preview.

Asset Pipeline Optimization

CSS Purging with Hugo

Unused CSS is a major source of bloat. Hugo can generate a statistics file and use it to purge unused styles.

{{/* layouts/partials/head.html */}}
{{ $styles := resources.Get "scss/main.scss" | resources.ToCSS }}

{{ if hugo.IsProduction }}
  {{ $styles = $styles | resources.PostCSS }}

  {{/* Purge unused CSS using Hugo's build stats */}}
  {{ $styles = $styles | resources.ExecuteAsTemplate "main.css" . }}
{{ end }}

{{ $styles = $styles | resources.Minify | resources.Fingerprint "sha256" }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity }}">

Expected behavior: In production, Hugo runs PostCSS (with Tailwind CSS purge configured), minifies the output, and fingerprints the file. Development builds skip minification and fingerprinting for faster iteration.

Image Optimization Pipeline

Images account for 60-80% of page weight. Hugo's image processing can generate responsive image sets at build time.

{{/* layouts/_default/_markup/render-image.html */}}
{{ $image := .Page.Resources.GetMatch (printf "**%s" .Destination) }}
{{ if $image }}
  {{ $small := $image.Resize "480x webp q80" }}
  {{ $medium := $image.Resize "768x webp q80" }}
  {{ $large := $image.Resize "1200x webp q80" }}
  <picture>
    <source srcset="{{ $small.RelPermalink }} 480w,
                    {{ $medium.RelPermalink }} 768w,
                    {{ $large.RelPermalink }} 1200w"
            sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1200px"
            type="image/webp">
    <img src="{{ $large.RelPermalink }}"
         alt="{{ .Text }}"
         loading="lazy"
         decoding="async"
         width="{{ $large.Width }}"
         height="{{ $large.Height }}">
  </picture>
{{ else }}
  <img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" loading="lazy">
{{ end }}

Expected behavior: Every image in a content page generates three WebP variants at 480px, 768px, and 1200px widths. The browser selects the appropriate size based on the viewport. The loading="lazy" attribute defers off-screen image loading until the user scrolls near them.

Caching Strategy Comparison

Cache Layer Duration Invalidation Strategy Impact
Hugo build cache 30 days Content hash change Reduces rebuild time 80%
CI/CD cache Per branch Manual clear Eliminates re-download of npm/Go modules
CDN edge cache 30 days Purge on deploy Zero TTFB for repeat visitors
Browser cache 1 year Fingerprinted URLs Instant loads for returning users
Service worker Runtime Version update Offline support + instant navigation

Common Errors

1. Not Using resources.Fingerprint for Cache Busting

Serving CSS/JS without fingerprinting means the browser caches the old version after a deploy. Users see broken styles until they hard-refresh. Always fingerprint assets and set aggressive Cache-Control headers.

2. Disabling Cache Compression in CI

GitHub Actions and other CI runners compress cache by default. Disabling compression or setting too-short cache durations forces full rebuilds every time. Configure CI cache keys to include only hash-sensitive inputs.

# .github/workflows/deploy.yml -- CI cache configuration
- name: Cache Hugo resources
  uses: actions/cache@v4
  with:
    path: /tmp/hugo_cache
    key: ${{ runner.os }}-hugo-${{ hashFiles('**/config/**') }}
    restore-keys: |
      ${{ runner.os }}-hugo-

Expected behavior: The CI runner caches Hugo's build resources keyed by the hash of config files. Config changes invalidate the cache; otherwise, the cache is restored from the most recent matching key.

3. Processing Images at Request Time Instead of Build Time

Using an external image service (like imgix or Cloudinary) for every image adds latency and cost. Process images at build time with the SSG's built-in pipeline for zero runtime overhead.

4. Ignoring Build Time Budgets

Without a build time budget, CI/CD times can drift from 30 seconds to 30 minutes unnoticed. Set alerts when build time exceeds a threshold. Use hugo --templateMetrics to identify slow templates.

5. Missing Cache-Control Headers on the CDN

Even with fingerprinted assets, missing Cache-Control: public, max-age=31536000, immutable on the CDN means the browser revalidates on every visit. Configure these headers in your _headers file or Cloudflare Pages settings.

Practice Questions

1. What is the purpose of resources.Fingerprint in Hugo?

It generates a content-hashed filename (e.g., styles.a1b2c3.css) so the filename changes when content changes. This enables aggressive long-term caching because the old URL is automatically invalidated.

2. How does Hugo's build cache reduce rebuild times?

It stores parsed templates, compiled SCSS, and processed images in resources/_gen. On subsequent builds, unchanged assets are loaded from cache instead of being reprocessed, reducing build time by up to 80%.

3. What is the difference between CDN edge cache and browser cache?

CDN edge cache stores content on global edge servers to reduce TTFB for visitors worldwide. Browser cache stores content on the user's device for instant repeat visits. Both layers work together -- CDN for first visit, browser for subsequent visits.

4. Why should images be processed at build time rather than request time?

Build-time processing (using Hugo Pipes, next/image, or Astro Image) generates optimized images once during deployment. Request-time processing adds latency for every user and requires external service calls. Build-time is zero runtime overhead.

5. Challenge: Set up a build that fails if total output exceeds 10MB or build time exceeds 30 seconds.

Use hugo --templateMetrics --minify in CI, parse the output with a script to measure build time and output size, and fail the pipeline if thresholds are exceeded.

Mini Project: CI/CD Build Pipeline with Caching

Create an optimized GitHub Actions workflow for a Hugo site:

  1. Set up dependency caching for Go modules and Hugo extended binary
  2. Configure Hugo build cache at /tmp/hugo_cache with a 30-day TTL
  3. Add a build step that fails if hugo --minify takes longer than 30 seconds
  4. Deploy to Cloudflare Pages with cache purge on successful deployment
  5. Add a check for oversized output (alert if _site/ exceeds 50MB)
name: Build and Deploy
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
          cache: true
      - uses: actions/cache@v4
        with:
          path: /tmp/hugo_cache
          key: hugo-cache-${{ hashFiles('config/**') }}
          restore-keys: hugo-cache-
      - run: |
          HUGO_CACHEDIR=/tmp/hugo_cache hugo --minify --gc 2>&1
      - run: |
          SIZE=$(du -sm public | cut -f1)
          if [ "$SIZE" -gt 50 ]; then
            echo "Error: Output exceeds 50MB ($SIZE MB)"
            exit 1
          fi

Test the workflow by pushing a branch with content changes and measuring the build time on the first run (cache miss) versus subsequent runs (cache hit).

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro