Skip to content

Commit bcbff3e

Browse files
authored
Set up automatic image optimization pipeline (#5779)
Use 11ty's first-party [image transformation plugin](https://www.11ty.dev/docs/plugins/image/) to automatically optimize images, convert them to `png`, `webp`, and `avif`, and then transform the site HTML to use the [`<picture>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture) to select the best one for the user. This only runs in production deploy builds to reduce serving time. Also applies lazy loading and async decoding to all images, even when serving without optimizations. The transform also adds a hash of the image, allowing us to expand caching for images in Firebase hosting. To account for the transformed HTML structure, some minor HTML and CSS changes were needed as well. Overall, this reduces page load time on pages that use images, and reduces unnecessary downloads that were due to a relatively short cache time for images. On a page with just a few images, such as https://dart.dev/tools/pub/automated-publishing, the lighthouse perf score increases around 10-15 points. Fixes #4473 Fixes #3124
1 parent 02853c7 commit bcbff3e

14 files changed

+427
-29
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
_cache/
22
_site/
3+
.cache
34
.*-cache
45
.*-metadata
56
.buildlog

eleventy.config.js

+35-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import yaml from 'js-yaml';
1919

2020
import * as path from 'node:path';
2121
import * as sass from 'sass';
22+
import {eleventyImageTransformPlugin} from '@11ty/eleventy-img';
2223

2324
// noinspection JSUnusedGlobalSymbols
2425
/**
@@ -27,6 +28,7 @@ import * as sass from 'sass';
2728
*/
2829
export default function (eleventyConfig) {
2930
const isProduction = process.env.PRODUCTION === 'true';
31+
const shouldOptimize = process.env.OPTIMIZE === 'true';
3032

3133
eleventyConfig.on('eleventy.before', async () => {
3234
await configureHighlighting(markdown);
@@ -71,7 +73,7 @@ export default function (eleventyConfig) {
7173
}
7274

7375
const result = sass.compileString(inputContent, {
74-
style: isProduction ? 'compressed' : 'expanded',
76+
style: shouldOptimize ? 'compressed' : 'expanded',
7577
quietDeps: true,
7678
loadPaths: [parsedPath.dir, 'src/_sass'],
7779
});
@@ -101,7 +103,7 @@ export default function (eleventyConfig) {
101103
'src/content/guides/language/specifications',
102104
);
103105

104-
if (isProduction) {
106+
if (shouldOptimize) {
105107
// If building for production, minify/optimize the HTML output.
106108
// Doing so during serving isn't worth the extra build time.
107109
eleventyConfig.addTransform('minify-html', async function (content) {
@@ -118,6 +120,37 @@ export default function (eleventyConfig) {
118120

119121
return content;
120122
});
123+
124+
// Optimize all images, generate an avif, webp, and png version,
125+
// and indicate they should be lazily loaded.
126+
// Save in `_site/assets/img` and update links to there.
127+
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
128+
extensions: 'html',
129+
formats: ['avif', 'webp', 'png', 'svg'],
130+
svgShortCircuit: true,
131+
widths: ['auto'],
132+
defaultAttributes: {
133+
loading: 'lazy',
134+
decoding: 'async',
135+
},
136+
urlPath: '/assets/img/',
137+
outputDir: '_site/assets/img/',
138+
});
139+
} else {
140+
// To be more consistent with the production build,
141+
// don't optimize images but still indicate they should be lazily loaded.
142+
// Then save in `_site/assets/img` and update links to there.
143+
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
144+
extensions: 'html',
145+
formats: ['auto'],
146+
widths: ['auto'],
147+
defaultAttributes: {
148+
loading: 'lazy',
149+
decoding: 'async',
150+
},
151+
urlPath: '/assets/img/',
152+
outputDir: '_site/assets/img/',
153+
});
121154
}
122155

123156
eleventyConfig.setQuietMode(true);

firebase.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
"trailingSlash": false,
66
"headers": [
77
{
8-
"source": "**/*.@(jpg|jpeg|gif|png|md|txt|json|webp|webm|svg|css|js)",
8+
"source": "**/*.@(avif|jpg|jpeg|gif|png|md|txt|json|webp|webm|svg|css|js)",
99
"headers": [
1010
{ "key": "Cache-Control", "value": "max-age=28800" },
1111
{ "key": "Access-Control-Allow-Origin", "value": "*" }
1212
]
1313
},
14+
{
15+
"source": "/assets/img/*.@(jpg|jpeg|png|webp|avif)",
16+
"headers": [
17+
{ "key": "Cache-Control", "value": "max-age=604800" },
18+
{ "key": "Access-Control-Allow-Origin", "value": "*" }
19+
]
20+
},
1421
{
1522
"source": "**",
1623
"headers": [

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
},
1111
"scripts": {
1212
"serve": "PRODUCTION=false eleventy --serve",
13-
"build-site-for-staging": "PRODUCTION=false eleventy",
14-
"build-site-for-production": "PRODUCTION=true eleventy"
13+
"build-site-for-staging": "PRODUCTION=false OPTIMIZE=true eleventy",
14+
"build-site-for-production": "PRODUCTION=true OPTIMIZE=true eleventy"
1515
},
1616
"engines": {
1717
"node": ">=20.10.0",
@@ -23,6 +23,7 @@
2323
},
2424
"devDependencies": {
2525
"@11ty/eleventy": "3.0.0-alpha.10",
26+
"@11ty/eleventy-img": "^4.0.2",
2627
"firebase-tools": "^13.10.2",
2728
"hast-util-from-html": "^2.0.1",
2829
"hast-util-select": "^6.0.2",

0 commit comments

Comments
 (0)