Minifying HTML, CSS and JS in Eleventy

Eleventy writes exactly the HTML your templates produce, whitespace and comments included, and it does not bundle or minify CSS and JavaScript by default. That transparency is one of its strengths and it also means minification is your job. It is a small job — a transform for HTML, a build step for CSS and JavaScript — but there are two ways to get it wrong: settings aggressive enough to break code blocks or inline layout, and expectations inflated by comparing uncompressed sizes when every response is compressed anyway.

This guide adds minification to an Eleventy 3 documentation site with 1,300 pages, measures what it actually saves over the wire, and shows the settings that keep <pre> blocks, inline elements and CSP hashes intact. It is part of CSS and JavaScript Asset Optimization, and the Eleventy build-speed side is covered in How to Reduce Bundle Size in Eleventy Builds.

Prerequisites

  • Eleventy 2 or 3, with CSS and JavaScript in a source folder (here src/assets/).
  • Node 20+, and html-minifier-terser, lightningcss and esbuild as dev dependencies.
  • Screenshot or HTML diff tooling to compare rendered output before and after.

Step 1: CSS With Lightning CSS

Lightning CSS minifies, bundles @imports and lowers modern syntax for the browsers you target, in a few milliseconds. Run it from Eleventy's eleventy.before event so it happens once per build:

// eleventy.config.js
import { bundle, browserslistToTargets } from 'lightningcss';
import browserslist from 'browserslist';
import { writeFile, mkdir } from 'node:fs/promises';

export default function (eleventyConfig) {
  eleventyConfig.on('eleventy.before', async () => {
    const { code } = bundle({
      filename: 'src/assets/css/site.css',
      minify: process.env.ELEVENTY_RUN_MODE === 'build',
      targets: browserslistToTargets(browserslist('>= 0.5%, last 2 versions, not dead'))
    });
    await mkdir('_site/assets', { recursive: true });
    await writeFile('_site/assets/site.css', code);
  });
  eleventyConfig.addWatchTarget('src/assets/css/');
}

The site's CSS went from 38.2 KB to 29.6 KB uncompressed (−23%) and from 8.4 KB to 7.9 KB with brotli (−6%).

Step 2: JavaScript With esbuild

The site's JavaScript is three small modules: theme toggle, copy-code buttons and a search loader. esbuild bundles and minifies them into ES modules in about 20 ms:

import * as esbuild from 'esbuild';
eleventyConfig.on('eleventy.before', async () => {
  await esbuild.build({
    entryPoints: ['src/assets/js/main.js', 'src/assets/js/search.js'],
    bundle: true, format: 'esm', splitting: true, outdir: '_site/assets/js',
    minify: process.env.ELEVENTY_RUN_MODE === 'build',
    target: 'es2020', sourcemap: 'linked', legalComments: 'none'
  });
});

splitting: true with format: 'esm' lets the search loader share code with the main module without duplicating it. Linked source maps are uploaded with the build but only fetched when DevTools is open.

Minification steps in the Eleventy build Before templates render, Lightning CSS bundles and minifies CSS and esbuild bundles and minifies JavaScript. Eleventy then renders 1,300 HTML pages. An HTML transform minifies each page in production builds only. Finally brotli precompression runs on the output. Where each file type is minified Lightning CSS bundle, minify, lower esbuild ESM, split, minify Render pages 1,300 HTML files HTML transform production builds only Precompress brotli 11
CSS and JS run once per build; the HTML transform runs once per page, which is why it is the step to skip in development.

Step 3: HTML With a Conservative Transform

HTML minification runs as an Eleventy transform on each output file. The options below are chosen to be safe for documentation content:

import { minify } from 'html-minifier-terser';

eleventyConfig.addTransform('htmlmin', async function (content) {
  if (process.env.ELEVENTY_RUN_MODE !== 'build') return content;
  if (!(this.page.outputPath || '').endsWith('.html')) return content;
  return minify(content, {
    collapseWhitespace: true,
    conservativeCollapse: true,     // keep one space where whitespace was, never join words
    removeComments: true,
    ignoreCustomComments: [/^\s*(pagefind|htmlmin:keep)/],
    collapseBooleanAttributes: true,
    removeRedundantAttributes: true,
    decodeEntities: true,
    minifyCSS: true,
    minifyJS: false,                // inline scripts are hashed for CSP; see below
    removeOptionalTags: false,
    sortAttributes: true,
    sortClassName: true
  });
});

html-minifier-terser never collapses whitespace inside <pre> and <textarea>, so code blocks survive. The two options that caused real breakage on this site in earlier attempts were collapseWhitespace without conservativeCollapse — it joined a link and the following word in several hundred places where a template put them on separate lines — and removeOptionalTags, which dropped closing </p> and </li> tags that a later Pagefind indexing step then mis-parsed. sortAttributes and sortClassName make no difference to correctness but improve brotli's matching slightly by making repeated attribute sequences identical.

Step 4: Inline Scripts and CSP Hashes

The site has a strict Content Security Policy with SHA-256 hashes for its two inline scripts (the theme bootstrap and the Pagefind loader). Any minification that changes those scripts after the hash is computed breaks them in every browser that enforces the policy. Two options: compute hashes after minification, or exclude inline scripts from minification and minify them once at source. This site does the latter — the inline scripts are written pre-minified in a partial, minifyJS is off, and the CSP hash step runs last in the build, over the final output. The approach mirrors Hash-Based CSP for Inline Scripts in Astro.

html-minifier-terser options grouped by risk Two panels. Safe options include removeComments with custom comments kept, conservative whitespace collapse, collapseBooleanAttributes, removeRedundantAttributes, minifyCSS and sorting attributes and class names. Risky options include aggressive whitespace collapse that joins words, removeOptionalTags that confuses later parsers, minifyJS on hashed inline scripts, and removing attribute quotes. Safe and risky minifier settings Safe for docs sites removeComments keeping pagefind markers conservativeCollapse collapseBooleanAttributes removeRedundantAttributes minifyCSS, sortAttributes Risky collapse without conservative mode removeOptionalTags minifyJS on hashed inline scripts removeAttributeQuotes
The risky options together saved under 1% more after brotli on this site, which is not worth a single broken page.

Step 5: Verify Nothing Changed Visually

Minification should be invisible. Compare a sample of pages before and after: screenshots with Playwright at mobile and desktop widths, plus a text diff of innerText for each page, which catches joined words that screenshots at a glance miss. On the conservative settings the text diff was empty across all 1,300 pages; on the aggressive settings it found 412 pages with joined words.

Measured Impact

Measured on the built output; transfer sizes with brotli level 11, summed over all pages or assets. Build time with hyperfine, five runs.

AssetUncompressed beforeAfterBrotli beforeAfter
HTML, all 1,300 pages71.4 MB55.7 MB (−22%)9.9 MB9.2 MB (−7%)
CSS38.2 KB29.6 KB (−23%)8.4 KB7.9 KB (−6%)
JS (3 modules)24.1 KB9.8 KB (−59%)7.2 KB3.6 KB (−50%)
Production build time9.1 s15.2 s——
Development rebuild (one page)0.21 s0.21 s (skipped)——
Percentage saved by minification uncompressed and with brotli Paired bars of percentage saved by minification. HTML saves 22 percent uncompressed but 7 percent after brotli. CSS saves 23 percent uncompressed and 6 percent after brotli. JavaScript saves 59 percent uncompressed and 50 percent after brotli, because minifying renames identifiers, which compression cannot do. Savings before and after compression Saved uncompressed Saved after brotli HTML 22% 7% CSS 23% 6% JavaScript 59% 50%
JavaScript is where minification still pays after compression; for HTML and CSS it is a small, cheap win.

JavaScript is the outlier because minifiers rename local variables and remove dead code — changes compression cannot achieve. For HTML and CSS, whitespace and comments are exactly the redundancy that brotli already eliminates, so minification saves little over the wire. It still saves parse work and storage, and it is cheap, so keep it.

When to Skip HTML Minification

HTML minification added six seconds to every production build, about 66% of the build time. For a site that deploys dozens of times a day from pull requests, that is real CI time for a 7% transfer saving. Two reasonable alternatives: run the HTML transform only for production deploys and skip it for preview builds (Eleventy exposes the environment through process.env, and preview builds can set a variable), or skip HTML minification entirely and rely on precompression at maximum brotli level, which on this site recovered most of the same saving at build time with better parallelism — see Precompressing Assets with Brotli and Zstd. This site kept HTML minification for production only and made preview builds six seconds faster.

For teams that want minification without writing transforms, the Eleventy Bundle plugin (built into Eleventy 3 as eleventyConfig.addBundle) can collect page-specific CSS and JavaScript from templates and run them through a transform hook, which is a convenient place to call Lightning CSS and esbuild.

Pitfalls & Rollback

  • Minifying in development. Slows every rebuild; gate on ELEVENTY_RUN_MODE.
  • Stripping comments that tools need. Pagefind and some analytics snippets rely on HTML comments; keep them with ignoreCustomComments.
  • Hashing before minifying. CSP hashes must be computed on the final bytes.
  • Minifying JSON-LD as JavaScript. Exclude application/ld+json scripts or minify them with JSON.stringify only.
  • Rollback: all three steps are guarded by the run mode; setting it to serve or removing the transform restores unminified output.

Conclusion

Minify everything Eleventy ships, but with conservative HTML settings and realistic expectations. JavaScript minification halves transfer size; HTML and CSS minification save 6–7% after brotli and cost build time, so run the HTML step only where it pays. Verify with a text diff, keep CSP hashing last, and keep code blocks and tool comments intact.

FAQ

How much does HTML minification save after compression?

Less than the raw numbers suggest. On a 1,300-page Eleventy documentation site, minifying HTML cut uncompressed size by 22 percent but brotli-compressed size by only 7 percent, because compression already removes most of the redundancy that whitespace represents.

Which html-minifier-terser options are risky?

Collapsing whitespace without conservative mode can join inline elements that relied on a space between them, removing optional tags can break tools that parse your HTML later, and minifying inline JavaScript or CSS can break content with templating syntax. Start with conservative settings and compare rendered screenshots.

Does minification slow down the Eleventy build?

HTML minification is the slowest part. With html-minifier-terser on 1,300 pages, the transform added about 6 seconds to a 9-second build. Skipping it during local development and running it only in production builds keeps the development loop fast.

Should I minify inline scripts and styles inside the HTML?

Yes if they are plain CSS and JavaScript, using Lightning CSS and esbuild or the minifier's built-in options. If the page has a Content Security Policy with hashes for inline scripts, minify before the hashes are computed, otherwise the hashes will no longer match.

Filed underHow-tosEleventy