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,lightningcssandesbuildas 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.
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.
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.
| Asset | Uncompressed before | After | Brotli before | After |
|---|---|---|---|---|
| HTML, all 1,300 pages | 71.4 MB | 55.7 MB (−22%) | 9.9 MB | 9.2 MB (−7%) |
| CSS | 38.2 KB | 29.6 KB (−23%) | 8.4 KB | 7.9 KB (−6%) |
| JS (3 modules) | 24.1 KB | 9.8 KB (−59%) | 7.2 KB | 3.6 KB (−50%) |
| Production build time | 9.1 s | 15.2 s | — | — |
| Development rebuild (one page) | 0.21 s | 0.21 s (skipped) | — | — |
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+jsonscripts or minify them withJSON.stringifyonly. - Rollback: all three steps are guarded by the run mode; setting it to
serveor 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.
Related
- Parent: CSS and JavaScript Asset Optimization — where minification fits.
- Precompressing Assets with Brotli and Zstd — the step that saves more.
- Enabling Incremental Builds in Eleventy — keeping builds fast as transforms add up.
- Purging Unused CSS with Tailwind and Lightning CSS — removing CSS before minifying it.
- Eleventy vs Jekyll for Markdown-Heavy Blogs — Eleventy's build model in context.