@hegemonart/get-design-done 1.16.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +12 -4
- package/.claude-plugin/plugin.json +22 -4
- package/CHANGELOG.md +111 -0
- package/README.md +27 -2
- package/agents/design-auditor.md +65 -1
- package/agents/design-context-builder.md +6 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +62 -0
- package/agents/design-phase-researcher.md +1 -1
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/package.json +16 -2
- package/reference/components/README.md +27 -23
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -0
- package/reference/components/chip.md +209 -0
- package/reference/components/command-palette.md +228 -0
- package/reference/components/date-picker.md +227 -0
- package/reference/components/file-upload.md +219 -0
- package/reference/components/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/rich-text-editor.md +226 -0
- package/reference/components/sidebar.md +211 -0
- package/reference/components/skeleton.md +197 -0
- package/reference/components/slider.md +208 -0
- package/reference/components/stepper.md +220 -0
- package/reference/components/table.md +229 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/data-visualization.md +333 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/form-patterns.md +245 -0
- package/reference/image-optimization.md +582 -0
- package/reference/information-architecture.md +255 -0
- package/reference/motion-advanced.md +754 -0
- package/reference/motion-easings.md +381 -0
- package/reference/motion-interpolate.md +282 -0
- package/reference/motion-spring.md +234 -0
- package/reference/motion-transition-taxonomy.md +155 -0
- package/reference/motion.md +20 -0
- package/reference/onboarding-progressive-disclosure.md +250 -0
- package/reference/output-contracts/motion-map.schema.json +135 -0
- package/reference/platforms.md +346 -0
- package/reference/registry.json +445 -220
- package/reference/registry.schema.json +4 -0
- package/reference/rtl-cjk-cultural.md +353 -0
- package/reference/user-research.md +360 -0
- package/reference/variable-fonts-loading.md +532 -0
- package/scripts/lib/easings.cjs +280 -0
- package/scripts/lib/parse-contract.cjs +220 -0
- package/scripts/lib/spring.cjs +160 -0
- package/scripts/tests/test-motion-provenance.sh +64 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
<!-- Source: Phase 18 — get-design-done -->
|
|
2
|
+
|
|
3
|
+
# Image Optimization Reference
|
|
4
|
+
|
|
5
|
+
Practical reference for responsive images, modern formats, loading strategies, and CDN transform patterns. Written for AI agents and developers working on web performance.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Format Choice Matrix
|
|
10
|
+
|
|
11
|
+
### When to Use Each Format
|
|
12
|
+
|
|
13
|
+
| Format | Browser Support | Compression vs JPEG | Encode Time | Alpha | Animation | Best Use Case |
|
|
14
|
+
|--------|----------------|---------------------|-------------|-------|-----------|---------------|
|
|
15
|
+
| **JPEG** | Universal (100%) | Baseline | Fast | No | No | Legacy fallback, wide compatibility required |
|
|
16
|
+
| **WebP** | 97%+ (all modern) | 25–35% smaller | Fast | Yes | Yes | Default for photos, general web images today |
|
|
17
|
+
| **AVIF** | 90%+ (Chrome 85+, Firefox 93+, Safari 16+) | 40–55% smaller than JPEG | Slow (CPU-heavy) | Yes | Yes | Photos where quality-per-byte is critical; static assets |
|
|
18
|
+
| **JPEG XL** | Limited (Chrome 91–109 behind flag; Safari 17+) | 20–60% smaller | Medium | Yes | No | Future-proofing; not yet viable as primary format |
|
|
19
|
+
| **PNG** | Universal (100%) | Lossless only | Fast | Yes | No | Logos, UI icons needing lossless or transparency |
|
|
20
|
+
| **SVG** | Universal (100%) | Vector = tiny | N/A | Yes | Yes | Icons, logos, illustrations with clean paths |
|
|
21
|
+
| **GIF** | Universal (100%) | Poor | Fast | 1-bit | Yes | Avoid — use WebP or video instead |
|
|
22
|
+
|
|
23
|
+
### Decision Rules
|
|
24
|
+
|
|
25
|
+
- **Default for photos**: serve AVIF with WebP fallback, JPEG last resort.
|
|
26
|
+
- **Default for UI assets with transparency**: WebP with PNG fallback.
|
|
27
|
+
- **Icons/logos**: SVG always; PNG fallback only for email.
|
|
28
|
+
- **Animated content**: WebP animated or `<video autoplay muted loop>` — never GIF.
|
|
29
|
+
- **JPEG XL**: Do not use as primary format until Safari + Chrome stable support lands.
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<!-- Standard format stack via <picture> -->
|
|
33
|
+
<picture>
|
|
34
|
+
<source srcset="hero.avif" type="image/avif">
|
|
35
|
+
<source srcset="hero.webp" type="image/webp">
|
|
36
|
+
<img src="hero.jpg" alt="Hero image" width="1200" height="630">
|
|
37
|
+
</picture>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 2. `srcset` and `sizes` Math
|
|
43
|
+
|
|
44
|
+
### Width Descriptors (w descriptors)
|
|
45
|
+
|
|
46
|
+
Width descriptors tell the browser the intrinsic pixel width of each candidate. The browser picks the best one based on the `sizes` attribute and device pixel ratio.
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<img
|
|
50
|
+
srcset="
|
|
51
|
+
image-480w.jpg 480w,
|
|
52
|
+
image-960w.jpg 960w,
|
|
53
|
+
image-1440w.jpg 1440w,
|
|
54
|
+
image-1920w.jpg 1920w
|
|
55
|
+
"
|
|
56
|
+
sizes="
|
|
57
|
+
(max-width: 480px) 480px,
|
|
58
|
+
(max-width: 960px) 960px,
|
|
59
|
+
(max-width: 1440px) 1440px,
|
|
60
|
+
1920px
|
|
61
|
+
"
|
|
62
|
+
src="image-1920w.jpg"
|
|
63
|
+
alt="Responsive photo"
|
|
64
|
+
width="1920"
|
|
65
|
+
height="1080"
|
|
66
|
+
>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### How to Calculate Descriptor Values
|
|
70
|
+
|
|
71
|
+
1. **Identify breakpoints** where layout changes (e.g., 480, 768, 960, 1280, 1920).
|
|
72
|
+
2. **For each breakpoint**, determine the rendered CSS width of the image.
|
|
73
|
+
3. **Multiply by the highest expected DPR** (typically 2x) to get the source pixel width.
|
|
74
|
+
4. **Round to the nearest common size** and generate that asset.
|
|
75
|
+
|
|
76
|
+
Example: image renders at 480px on mobile at 2x DPR → need a 960px source → label it `960w`.
|
|
77
|
+
|
|
78
|
+
### Sizes Attribute Math
|
|
79
|
+
|
|
80
|
+
`sizes` is evaluated left-to-right; first matching condition wins. Express as CSS `calc()` when the image is a fraction of the viewport:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<!-- Image is full-width on mobile, 50vw on tablet, 33vw on desktop -->
|
|
84
|
+
<img
|
|
85
|
+
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w, photo-1600.jpg 1600w"
|
|
86
|
+
sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
87
|
+
src="photo-1200.jpg"
|
|
88
|
+
alt="Gallery photo"
|
|
89
|
+
>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Breakpoint widths to always generate sources for:
|
|
93
|
+
- `480w` — small mobile
|
|
94
|
+
- `768w` — large mobile / portrait tablet
|
|
95
|
+
- `960w` — landscape tablet / small desktop
|
|
96
|
+
- `1280w` — standard desktop
|
|
97
|
+
- `1920w` — full HD desktop
|
|
98
|
+
- `2560w` — retina desktop / 2x of 1280
|
|
99
|
+
|
|
100
|
+
### Density Descriptors (x descriptors)
|
|
101
|
+
|
|
102
|
+
Use `x` descriptors only when the image always renders at a fixed CSS size (e.g., a fixed-width avatar or logo). Do not mix `w` and `x` in the same `srcset`.
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<!-- Fixed 80×80px avatar — use density descriptors -->
|
|
106
|
+
<img
|
|
107
|
+
srcset="avatar.jpg 1x, avatar@2x.jpg 2x, avatar@3x.jpg 3x"
|
|
108
|
+
src="avatar.jpg"
|
|
109
|
+
alt="User avatar"
|
|
110
|
+
width="80"
|
|
111
|
+
height="80"
|
|
112
|
+
>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Rule of thumb**: use `w` descriptors + `sizes` for fluid/responsive images; use `x` descriptors for fixed-size images.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 3. Responsive Art Direction
|
|
120
|
+
|
|
121
|
+
Art direction changes the image content (crop, composition, subject) at different viewport sizes — not just resolution. Use `<picture>` with `media` attributes.
|
|
122
|
+
|
|
123
|
+
### `<picture>` + `<source>` Pattern
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<picture>
|
|
127
|
+
<!-- Portrait crop for mobile -->
|
|
128
|
+
<source
|
|
129
|
+
media="(max-width: 600px)"
|
|
130
|
+
srcset="hero-portrait-600.webp 600w, hero-portrait-1200.webp 1200w"
|
|
131
|
+
sizes="100vw"
|
|
132
|
+
type="image/webp"
|
|
133
|
+
>
|
|
134
|
+
<source
|
|
135
|
+
media="(max-width: 600px)"
|
|
136
|
+
srcset="hero-portrait-600.jpg 600w, hero-portrait-1200.jpg 1200w"
|
|
137
|
+
sizes="100vw"
|
|
138
|
+
>
|
|
139
|
+
|
|
140
|
+
<!-- Landscape crop for desktop -->
|
|
141
|
+
<source
|
|
142
|
+
media="(min-width: 601px)"
|
|
143
|
+
srcset="hero-landscape-1200.webp 1200w, hero-landscape-1920.webp 1920w"
|
|
144
|
+
sizes="100vw"
|
|
145
|
+
type="image/webp"
|
|
146
|
+
>
|
|
147
|
+
<source
|
|
148
|
+
media="(min-width: 601px)"
|
|
149
|
+
srcset="hero-landscape-1200.jpg 1200w, hero-landscape-1920.jpg 1920w"
|
|
150
|
+
sizes="100vw"
|
|
151
|
+
>
|
|
152
|
+
|
|
153
|
+
<!-- Fallback <img> — always required, used when no <source> matches -->
|
|
154
|
+
<img
|
|
155
|
+
src="hero-landscape-1200.jpg"
|
|
156
|
+
alt="Team working in studio"
|
|
157
|
+
width="1200"
|
|
158
|
+
height="630"
|
|
159
|
+
>
|
|
160
|
+
</picture>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Source Order Rules
|
|
164
|
+
|
|
165
|
+
1. `<source>` elements are evaluated top-to-bottom; first match wins.
|
|
166
|
+
2. Place mobile/narrow sources first when using `max-width` media queries.
|
|
167
|
+
3. Always end with a plain `<img>` — it is the fallback AND the element browsers use for accessibility attributes.
|
|
168
|
+
4. The `type` attribute on `<source>` enables format negotiation without JS.
|
|
169
|
+
|
|
170
|
+
### Combining Art Direction + Format Negotiation
|
|
171
|
+
|
|
172
|
+
```html
|
|
173
|
+
<picture>
|
|
174
|
+
<!-- Mobile: square crop, AVIF -->
|
|
175
|
+
<source media="(max-width: 480px)" srcset="product-sq.avif" type="image/avif">
|
|
176
|
+
<source media="(max-width: 480px)" srcset="product-sq.webp" type="image/webp">
|
|
177
|
+
<source media="(max-width: 480px)" srcset="product-sq.jpg">
|
|
178
|
+
|
|
179
|
+
<!-- Desktop: wide crop, AVIF -->
|
|
180
|
+
<source srcset="product-wide.avif" type="image/avif">
|
|
181
|
+
<source srcset="product-wide.webp" type="image/webp">
|
|
182
|
+
|
|
183
|
+
<img src="product-wide.jpg" alt="Product on white background" width="1200" height="600">
|
|
184
|
+
</picture>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 4. Placeholder Strategies (LQIP / BlurHash / ThumbHash)
|
|
190
|
+
|
|
191
|
+
Placeholders prevent layout shift and give perceived performance improvement during image load.
|
|
192
|
+
|
|
193
|
+
### LQIP (Low-Quality Image Placeholder)
|
|
194
|
+
|
|
195
|
+
A tiny JPEG (typically 20–40px wide) embedded as a base64 data URI, displayed blurred via CSS until the full image loads.
|
|
196
|
+
|
|
197
|
+
**Implementation:**
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<div class="img-wrapper">
|
|
201
|
+
<img
|
|
202
|
+
class="lqip"
|
|
203
|
+
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD..."
|
|
204
|
+
aria-hidden="true"
|
|
205
|
+
alt=""
|
|
206
|
+
>
|
|
207
|
+
<img
|
|
208
|
+
class="full"
|
|
209
|
+
src="photo.jpg"
|
|
210
|
+
srcset="photo-480.jpg 480w, photo-960.jpg 960w"
|
|
211
|
+
sizes="(max-width: 600px) 100vw, 50vw"
|
|
212
|
+
alt="Studio workspace"
|
|
213
|
+
loading="lazy"
|
|
214
|
+
onload="this.previousElementSibling.style.opacity=0"
|
|
215
|
+
>
|
|
216
|
+
</div>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```css
|
|
220
|
+
.img-wrapper { position: relative; overflow: hidden; }
|
|
221
|
+
.lqip { position: absolute; inset: 0; width: 100%; height: 100%;
|
|
222
|
+
object-fit: cover; filter: blur(20px); transition: opacity 0.3s; }
|
|
223
|
+
.full { display: block; width: 100%; }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Trade-offs**: Simple, no JS library needed. Base64 adds ~33% size overhead. LQIP JPEG should be under 300 bytes (20–40px wide, quality 20).
|
|
227
|
+
|
|
228
|
+
### BlurHash
|
|
229
|
+
|
|
230
|
+
A compact string (20–30 chars) encoding a blurred preview. Decoded client-side via the `blurhash` library into a canvas or CSS gradient.
|
|
231
|
+
|
|
232
|
+
**Implementation:**
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
import { decode } from 'blurhash';
|
|
236
|
+
|
|
237
|
+
function renderBlurHash(hash, width = 32, height = 32) {
|
|
238
|
+
const pixels = decode(hash, width, height);
|
|
239
|
+
const canvas = document.createElement('canvas');
|
|
240
|
+
canvas.width = width;
|
|
241
|
+
canvas.height = height;
|
|
242
|
+
const ctx = canvas.getContext('2d');
|
|
243
|
+
const imageData = ctx.createImageData(width, height);
|
|
244
|
+
imageData.data.set(pixels);
|
|
245
|
+
ctx.putImageData(imageData, 0, 0);
|
|
246
|
+
return canvas.toDataURL();
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```html
|
|
251
|
+
<!-- Store hash in data attribute; render via JS -->
|
|
252
|
+
<img
|
|
253
|
+
data-blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
|
|
254
|
+
src="photo.jpg"
|
|
255
|
+
alt="Photo"
|
|
256
|
+
loading="lazy"
|
|
257
|
+
>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Trade-offs**: Smaller than LQIP (string vs base64 blob). Requires JS to decode. Good for CMS-driven images where hashes are stored in metadata. Generate server-side with `sharp` + `blurhash` npm package.
|
|
261
|
+
|
|
262
|
+
### ThumbHash
|
|
263
|
+
|
|
264
|
+
Newer algorithm (2023). Preserves aspect ratio in the hash, better color accuracy for fine details and alpha channels. Drop-in replacement for BlurHash.
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
import { thumbHashToDataURL } from 'thumbhash';
|
|
268
|
+
|
|
269
|
+
// hash is a Uint8Array stored alongside image metadata
|
|
270
|
+
const placeholderURL = thumbHashToDataURL(hash);
|
|
271
|
+
img.src = placeholderURL; // set before lazy-load fires
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Trade-offs**: Slightly larger hash (~28 bytes binary vs BlurHash string) but visually superior. Choose ThumbHash for new projects; BlurHash for existing integrations.
|
|
275
|
+
|
|
276
|
+
### Comparison Table
|
|
277
|
+
|
|
278
|
+
| Strategy | Size overhead | JS required | Alpha support | Aspect ratio encoded | Best for |
|
|
279
|
+
|----------|---------------|-------------|---------------|----------------------|----------|
|
|
280
|
+
| LQIP | ~200–400 bytes base64 | No | No | No | Simple static sites |
|
|
281
|
+
| BlurHash | ~20–30 char string | Yes | No | No | CMS/dynamic images |
|
|
282
|
+
| ThumbHash | ~28 bytes binary | Yes | Yes | Yes | New projects, transparent images |
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 5. Lazy Loading
|
|
287
|
+
|
|
288
|
+
### `loading="lazy"` Attribute
|
|
289
|
+
|
|
290
|
+
Native browser lazy loading. Defers fetching images outside the viewport.
|
|
291
|
+
|
|
292
|
+
```html
|
|
293
|
+
<!-- Apply to all non-critical images -->
|
|
294
|
+
<img src="photo.jpg" alt="Gallery item" loading="lazy" width="800" height="600">
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Critical rule**: Never apply `loading="lazy"` to the LCP (Largest Contentful Paint) image. The LCP image must be fetched immediately. Identify it as the largest above-the-fold image — hero, product shot, or article lead image.
|
|
298
|
+
|
|
299
|
+
### Intersection Observer Pattern
|
|
300
|
+
|
|
301
|
+
For fine-grained control or browsers that need older support:
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
const lazyImages = document.querySelectorAll('img[data-src]');
|
|
305
|
+
|
|
306
|
+
const observer = new IntersectionObserver((entries, obs) => {
|
|
307
|
+
entries.forEach(entry => {
|
|
308
|
+
if (!entry.isIntersecting) return;
|
|
309
|
+
const img = entry.target;
|
|
310
|
+
img.src = img.dataset.src;
|
|
311
|
+
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
|
|
312
|
+
img.removeAttribute('data-src');
|
|
313
|
+
img.removeAttribute('data-srcset');
|
|
314
|
+
obs.unobserve(img);
|
|
315
|
+
});
|
|
316
|
+
}, {
|
|
317
|
+
rootMargin: '200px 0px', // Start loading 200px before entering viewport
|
|
318
|
+
threshold: 0
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
lazyImages.forEach(img => observer.observe(img));
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```html
|
|
325
|
+
<!-- Use data-src / data-srcset to prevent immediate fetch -->
|
|
326
|
+
<img
|
|
327
|
+
data-src="photo.jpg"
|
|
328
|
+
data-srcset="photo-480.jpg 480w, photo-960.jpg 960w"
|
|
329
|
+
sizes="(max-width: 600px) 100vw, 50vw"
|
|
330
|
+
alt="Gallery photo"
|
|
331
|
+
width="960"
|
|
332
|
+
height="640"
|
|
333
|
+
>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Lazy Loading Rules Summary
|
|
337
|
+
|
|
338
|
+
- `loading="lazy"` on all below-the-fold images.
|
|
339
|
+
- Never on LCP image — use `fetchpriority="high"` instead.
|
|
340
|
+
- Set explicit `width` and `height` on every `<img>` to prevent layout shift (CLS).
|
|
341
|
+
- `rootMargin: '200px'` is a safe default for IntersectionObserver to preload slightly early.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 6. `decoding="async"`
|
|
346
|
+
|
|
347
|
+
### What It Does
|
|
348
|
+
|
|
349
|
+
`decoding="async"` tells the browser it can decode the image off the main thread, allowing other rendering work to proceed in parallel. Without it, browsers may block rendering while decoding a large image.
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
<img src="photo.jpg" alt="Decoded async" decoding="async" width="1200" height="800">
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### When to Apply
|
|
356
|
+
|
|
357
|
+
- Apply to all large images that are not LCP-critical.
|
|
358
|
+
- **Do not** apply to the LCP image — synchronous decoding ensures the image renders as soon as it loads, which improves LCP score.
|
|
359
|
+
- Combine with `loading="lazy"` on below-the-fold images.
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<!-- LCP image: no async decoding, no lazy loading -->
|
|
363
|
+
<img
|
|
364
|
+
src="hero.jpg"
|
|
365
|
+
alt="Hero"
|
|
366
|
+
fetchpriority="high"
|
|
367
|
+
width="1920"
|
|
368
|
+
height="1080"
|
|
369
|
+
>
|
|
370
|
+
|
|
371
|
+
<!-- Below-fold images: async decoding + lazy loading -->
|
|
372
|
+
<img
|
|
373
|
+
src="card.jpg"
|
|
374
|
+
alt="Card"
|
|
375
|
+
decoding="async"
|
|
376
|
+
loading="lazy"
|
|
377
|
+
width="400"
|
|
378
|
+
height="300"
|
|
379
|
+
>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## 7. `fetchpriority="high"`
|
|
385
|
+
|
|
386
|
+
### What It Does
|
|
387
|
+
|
|
388
|
+
`fetchpriority="high"` elevates the image's network priority in the browser's resource scheduler. Applied to the LCP image, it reduces the time the browser spends waiting for it, directly improving LCP Core Web Vital.
|
|
389
|
+
|
|
390
|
+
```html
|
|
391
|
+
<!-- Boost the LCP image -->
|
|
392
|
+
<img
|
|
393
|
+
src="hero.jpg"
|
|
394
|
+
srcset="hero-960.jpg 960w, hero-1920.jpg 1920w"
|
|
395
|
+
sizes="100vw"
|
|
396
|
+
alt="Hero"
|
|
397
|
+
fetchpriority="high"
|
|
398
|
+
width="1920"
|
|
399
|
+
height="1080"
|
|
400
|
+
>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Rules
|
|
404
|
+
|
|
405
|
+
- Apply to **exactly one** image per page — the LCP candidate.
|
|
406
|
+
- Do not use on multiple images; it degrades priority scheduling.
|
|
407
|
+
- Use `fetchpriority="low"` on decorative or off-screen images you know will not be needed soon.
|
|
408
|
+
- Works on `<img>`, `<link rel="preload">`, and `fetch()` calls.
|
|
409
|
+
|
|
410
|
+
```html
|
|
411
|
+
<!-- Preload LCP image in <head> for maximum early fetch -->
|
|
412
|
+
<link
|
|
413
|
+
rel="preload"
|
|
414
|
+
as="image"
|
|
415
|
+
href="hero.jpg"
|
|
416
|
+
imagesrcset="hero-960.jpg 960w, hero-1920.jpg 1920w"
|
|
417
|
+
imagesizes="100vw"
|
|
418
|
+
fetchpriority="high"
|
|
419
|
+
>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 8. CDN Transform Patterns
|
|
425
|
+
|
|
426
|
+
### Cloudinary
|
|
427
|
+
|
|
428
|
+
Uses URL-based transformation parameters. `f_auto` selects the best format; `q_auto` selects optimal quality.
|
|
429
|
+
|
|
430
|
+
```html
|
|
431
|
+
<!-- Base URL structure: https://res.cloudinary.com/{cloud_name}/image/upload/{transformations}/{public_id} -->
|
|
432
|
+
|
|
433
|
+
<!-- Auto format + auto quality -->
|
|
434
|
+
<img
|
|
435
|
+
src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/sample.jpg"
|
|
436
|
+
alt="Auto-optimized"
|
|
437
|
+
>
|
|
438
|
+
|
|
439
|
+
<!-- Responsive srcset with Cloudinary transforms -->
|
|
440
|
+
<img
|
|
441
|
+
srcset="
|
|
442
|
+
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_480/sample.jpg 480w,
|
|
443
|
+
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_960/sample.jpg 960w,
|
|
444
|
+
https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_1920/sample.jpg 1920w
|
|
445
|
+
"
|
|
446
|
+
sizes="(max-width: 600px) 100vw, 50vw"
|
|
447
|
+
src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_1200/sample.jpg"
|
|
448
|
+
alt="Cloudinary responsive"
|
|
449
|
+
>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Key Cloudinary params: `f_auto` (format), `q_auto` (quality), `w_N` (width), `h_N` (height), `c_fill` (crop mode), `g_auto` (smart gravity/focus).
|
|
453
|
+
|
|
454
|
+
### imgix
|
|
455
|
+
|
|
456
|
+
Similar URL-based API. `auto=format` selects best format; `auto=compress` applies smart compression.
|
|
457
|
+
|
|
458
|
+
```html
|
|
459
|
+
<!-- Auto format + compress -->
|
|
460
|
+
<img
|
|
461
|
+
src="https://yoursite.imgix.net/photo.jpg?auto=format,compress"
|
|
462
|
+
alt="imgix optimized"
|
|
463
|
+
>
|
|
464
|
+
|
|
465
|
+
<!-- Responsive srcset with imgix -->
|
|
466
|
+
<img
|
|
467
|
+
srcset="
|
|
468
|
+
https://yoursite.imgix.net/photo.jpg?auto=format,compress&w=480 480w,
|
|
469
|
+
https://yoursite.imgix.net/photo.jpg?auto=format,compress&w=960 960w,
|
|
470
|
+
https://yoursite.imgix.net/photo.jpg?auto=format,compress&w=1920 1920w
|
|
471
|
+
"
|
|
472
|
+
sizes="(max-width: 600px) 100vw, 50vw"
|
|
473
|
+
src="https://yoursite.imgix.net/photo.jpg?auto=format,compress&w=1200"
|
|
474
|
+
alt="imgix responsive"
|
|
475
|
+
>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Key imgix params: `auto=format` (WebP/AVIF negotiation), `auto=compress`, `w`, `h`, `fit=crop`, `fp-x`/`fp-y` (focal point).
|
|
479
|
+
|
|
480
|
+
### Vercel / Next.js Image (`next/image`)
|
|
481
|
+
|
|
482
|
+
The `<Image>` component handles `srcset`, lazy loading, format negotiation, and the `/_next/image` optimization endpoint automatically.
|
|
483
|
+
|
|
484
|
+
```jsx
|
|
485
|
+
import Image from 'next/image';
|
|
486
|
+
|
|
487
|
+
// Responsive image — fill container
|
|
488
|
+
<div style={{ position: 'relative', width: '100%', aspectRatio: '16/9' }}>
|
|
489
|
+
<Image
|
|
490
|
+
src="/photos/hero.jpg"
|
|
491
|
+
alt="Hero"
|
|
492
|
+
fill
|
|
493
|
+
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
494
|
+
priority // equivalent to fetchpriority="high" — use for LCP image
|
|
495
|
+
quality={85} // default is 75; 80–90 for hero images
|
|
496
|
+
/>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
// Fixed-size image
|
|
500
|
+
<Image
|
|
501
|
+
src="/avatars/user.jpg"
|
|
502
|
+
alt="User avatar"
|
|
503
|
+
width={80}
|
|
504
|
+
height={80}
|
|
505
|
+
quality={80}
|
|
506
|
+
/>
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Next.js generates transform URLs like: `/_next/image?url=%2Fphotos%2Fhero.jpg&w=1920&q=85`
|
|
510
|
+
|
|
511
|
+
### CDN Comparison
|
|
512
|
+
|
|
513
|
+
| Feature | Cloudinary | imgix | Vercel (next/image) |
|
|
514
|
+
|---------|-----------|-------|---------------------|
|
|
515
|
+
| Auto format (WebP/AVIF) | `f_auto` | `auto=format` | Automatic |
|
|
516
|
+
| Auto quality | `q_auto` | `auto=compress` | `quality` prop (default 75) |
|
|
517
|
+
| Art direction / crop | `c_fill`, `g_auto` | `fit=crop`, `fp-x` | `object-fit` via CSS |
|
|
518
|
+
| Responsive srcset | Manual URL params | Manual URL params | Automatic via `sizes` |
|
|
519
|
+
| Setup required | Cloudinary account | imgix source | Next.js project |
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## 9. Image Budget Enforcement
|
|
524
|
+
|
|
525
|
+
### Maximum File Sizes by Image Type
|
|
526
|
+
|
|
527
|
+
| Image Type | Max Compressed Size | Max Dimensions | Notes |
|
|
528
|
+
|------------|--------------------|--------------------|-------|
|
|
529
|
+
| Hero / full-width banner | 200 KB | 1920 × 1080px | WebP/AVIF; 1x size, rely on srcset for 2x |
|
|
530
|
+
| Article / blog lead image | 100 KB | 1200 × 630px | Open Graph dimensions; dual purpose |
|
|
531
|
+
| Card / thumbnail | 40 KB | 600 × 400px | At displayed size, not source size |
|
|
532
|
+
| Avatar / profile photo | 15 KB | 200 × 200px | Square; serve WebP |
|
|
533
|
+
| Product image | 150 KB | 1000 × 1000px | White background; support zoom |
|
|
534
|
+
| Icon (raster fallback) | 5 KB | 64 × 64px | Prefer SVG; PNG only for emoji/photo icons |
|
|
535
|
+
| LQIP placeholder | 0.5 KB | 20–40px wide | Base64-embedded; blur via CSS |
|
|
536
|
+
| Logo (raster) | 20 KB | 400 × 200px | SVG strongly preferred |
|
|
537
|
+
|
|
538
|
+
### Tooling
|
|
539
|
+
|
|
540
|
+
**Build-time optimization:**
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
# sharp (Node.js) — resize + convert
|
|
544
|
+
npx sharp-cli --input "src/**/*.{jpg,png}" --output dist/images \
|
|
545
|
+
--format webp --quality 82 --width 1920
|
|
546
|
+
|
|
547
|
+
# squoosh-cli — AVIF + WebP batch encoding
|
|
548
|
+
npx @squoosh/cli --avif '{"quality":60}' --webp '{"quality":82}' src/*.jpg
|
|
549
|
+
|
|
550
|
+
# imagemin — plugin-based pipeline
|
|
551
|
+
npx imagemin src/images/* --out-dir=dist/images \
|
|
552
|
+
--plugin=imagemin-webp --plugin=imagemin-mozjpeg
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**CI enforcement (example GitHub Actions step):**
|
|
556
|
+
|
|
557
|
+
```yaml
|
|
558
|
+
- name: Check image sizes
|
|
559
|
+
run: |
|
|
560
|
+
find dist/images -name "*.jpg" -size +200k -print | while read f; do
|
|
561
|
+
echo "FAIL: $f exceeds 200KB hero budget"
|
|
562
|
+
exit 1
|
|
563
|
+
done
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Audit tools:**
|
|
567
|
+
|
|
568
|
+
- Lighthouse `uses-optimized-images` and `uses-webp-images` audits
|
|
569
|
+
- `next/image` built-in warnings for missing `width`/`height`
|
|
570
|
+
- `imagesize` npm package for build-time dimension checks
|
|
571
|
+
- Squoosh web app for manual before/after quality comparison
|
|
572
|
+
|
|
573
|
+
### Enforcement Checklist
|
|
574
|
+
|
|
575
|
+
- [ ] All hero images: AVIF + WebP + JPEG stack via `<picture>`
|
|
576
|
+
- [ ] Every `<img>` has explicit `width` and `height` attributes (prevents CLS)
|
|
577
|
+
- [ ] LCP image: `fetchpriority="high"`, no `loading="lazy"`, preload in `<head>`
|
|
578
|
+
- [ ] All below-fold images: `loading="lazy"` + `decoding="async"`
|
|
579
|
+
- [ ] No image exceeds budget for its type (see table above)
|
|
580
|
+
- [ ] CDN delivers `Content-Type: image/avif` or `image/webp` where supported
|
|
581
|
+
- [ ] Placeholders (LQIP or hash) on images with visible load time
|
|
582
|
+
- [ ] `sizes` attribute present on all images using `w` descriptors in `srcset`
|