Skip to content

Commit a6ff9b8

Browse files
committed
Revert "automatic sizes"
This reverts commit 05f4372.
1 parent 05f4372 commit a6ff9b8

File tree

5 files changed

+120
-52
lines changed

5 files changed

+120
-52
lines changed

documentation/docs/30-advanced/60-images.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,9 @@ Use in your `.svelte` components by using `<enhanced:img>` rather than `<img>` a
5959
<enhanced:img src="./path/to/your/image.jpg" alt="An alt text" />
6060
```
6161

62-
At build time, your `<enhanced:img>` tag will be replaced with an `<img>` wrapped by a `<picture>` providing multiple image types and sizes. It's only possible to downscale images without losing quality, which means that you should provide the highest resolution image that you need — smaller versions will be generated for the various device types that may request an image.
62+
At build time, your `<enhanced:img>` tag will be replaced with an `<img>` wrapped by a `<picture>` providing multiple image types and sizes. It's only possible to downscale images without losing quality, which means that you should provide the highest resolution image that you need — smaller versions will be generated for the various device types that may request an image. If you're not using the [`sizes` attribute](#sveltejs-enhanced-img-srcset-and-sizes) you should provide your image at 2x resolution for HiDPI displays (a.k.a. retina displays).
6363

64-
You should provide your image at 2x resolution for HiDPI displays (a.k.a. retina displays). `<enhanced:img>` will automatically take care of serving smaller versions to smaller devices.
65-
66-
If you wish to add styles to your `<enhanced:img>`, you should add a `class` and target that.
64+
Since the `<enhanced:img>` element is converted to an `<img>` element, you can style it with an `img {...}` CSS rule, but you may find it more natural to add a `class` name and target that.
6765

6866
### Dynamically choosing an image
6967

@@ -105,17 +103,37 @@ const pictures = import.meta.glob(
105103

106104
### `srcset` and `sizes`
107105

108-
`<enhanced:img>` will generate different width images and corresponding `srcset` and `sizes` attributes, so that smaller versions of your image will be served to smaller devices.
106+
If you have a large image, such as a hero image taking the width of the design, you should specify `sizes` so that smaller versions are requested on smaller devices. This would typically look like:
107+
108+
```html
109+
<img
110+
srcset="image-640.png 640w, image-750.png 750w, image-828.png 828w, image-1080.png 1080w, image-1200.png 1200w, image-1280.png 1280w"
111+
sizes="(min-width:1280px) 1280px, 100vw"
112+
/>
113+
```
114+
115+
In this example, it would be tedious to have to manually create half a dozen versions of your image, so we'll generate the `srcset` for you when you specify `sizes`.
116+
117+
```svelte
118+
<enhanced:img
119+
src="./image.png"
120+
sizes="(min-width:1280px) 1280px, 100vw"
121+
/>
122+
```
109123

110-
If you specify `sizes` it will take precedence over the default provided by `<enhanced:img>`, and you can also specify custom widths with the `w` query parameter:
124+
If you'd like to specify custom widths of a particular image you can do that with the `w` query parameter:
111125
```svelte
112126
<enhanced:img
113127
src="./image.png?w=1280;640;400"
114128
sizes="(min-width:1280px) 1280px, 100vw"
115129
/>
116130
```
117131

118-
Remember that the base image you provide should be 2x the resolution you wish to display so that the browser can better display the image on devices with a high [device pixel ratio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio).
132+
If `sizes` is specified directly as a string on the `<enhanced:img>` tag then the plugin will generate different width images and a corresponding `srcset`. If some of the `sizes` have been specified as a percentage of the viewport width using the `vw` unit then the `srcset` will filter out any values which are too small to ever be requested by the browser.
133+
134+
If `sizes` is not provided, then a HiDPI/Retina image and a standard resolution image will be generated. The image you provide should be 2x the resolution you wish to display so that the browser can display that image on devices with a high [device pixel ratio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio).
135+
136+
> Dynamic expressions like `sizes={computedSizes}` will not be evaluated for the purposes of automatic image generation and will be skipped.
119137
120138
### Per-image transforms
121139

@@ -135,10 +153,10 @@ Using a content delivery network (CDN) can allow you to optimize these images dy
135153

136154
## Best practices
137155

138-
- For each image type, use the appropriate solution from those discussed above. You can mix and match all three solutions in one project. For example, you may use Vite's built-in handling to provide images for `<meta>` tags, display images on your homepage with `@sveltejs/enhanced-img`, and display user-submitted content with a dynamic approach.
139-
- Consider serving all images via CDN regardless of the image optimization types you use. CDNs reduce latency by distributing copies of static assets globally.
156+
- Always provide a good `alt` text
140157
- Your original images should have a good quality/resolution and should have 2x the width it will be displayed at to serve HiDPI devices. Image processing can size images down to save bandwidth when serving smaller screens, but it would be a waste of bandwidth to invent pixels to size images up.
141158
- Give the image a container or styling so that it is constrained and does not jump around. `width` and `height` help the browser reserving space while the image is still loading. `@sveltejs/enhanced-img` will add a `width` and `height` for you.
142-
- For images which are much larger than the width of a mobile device (roughly 400px), such as a hero image taking the width of the page design, specify `sizes` so that smaller images can be served on smaller devices. `@sveltejs/enhanced-img` will do this for you.
159+
- For images which are much larger than the width of a mobile device (roughly 400px), such as a hero image taking the width of the page design, specify `sizes` so that smaller images can be served on smaller devices.
143160
- Choose one image per page which is the most important/largest one and give it `priority` so it loads faster. This gives you better web vitals scores (largest contentful paint in particular).
144-
- Always provide a good `alt` text. The Svelte compiler will warn you if you don't do this.
161+
- For each image type, use the appropriate solution from those discussed above. You can mix and match all three solutions in one project. For example, you may use Vite's built-in handling to provide images for `<meta>` tags, display images on your homepage with `@sveltejs/enhanced-img`, and display user-submitted content with a dynamic approach.
162+
- Consider serving all images via CDN regardless of the image optimization types you use. CDNs reduce latency by distributing copies of static assets globally.

packages/enhanced-img/src/index.js

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,15 @@ async function imagetools() {
7070
/** @type {Partial<import('vite-imagetools').VitePluginOptions>} */
7171
const imagetools_opts = {
7272
defaultDirectives: async ({ pathname, searchParams: qs }, metadata) => {
73+
const { imgSizes, imgWidth } = Object.fromEntries(qs);
7374
if (!qs.has('enhanced')) return new URLSearchParams();
7475

76+
const { widths, kind } = getWidths(imgWidth ?? (await metadata()).width, imgSizes);
7577
return new URLSearchParams({
7678
as: 'picture',
7779
format: `avif;webp;${fallback[path.extname(pathname)] ?? 'png'}`,
78-
w: get_widths(qs.get('imgWidth') ?? (await metadata()).width).join(';')
80+
w: widths.join(';'),
81+
...(kind === 'x' && !qs.has('w') && { basePixels: widths[0].toString() })
7982
});
8083
},
8184
namedExports: false
@@ -88,21 +91,54 @@ async function imagetools() {
8891
}
8992

9093
/**
94+
* Derived from
95+
* https://github.com/vercel/next.js/blob/3f25a2e747fc27da6c2166e45d54fc95e96d7895/packages/next/src/shared/lib/get-img-props.ts#L132
96+
* under the MIT license. Copyright (c) Vercel, Inc.
9197
* @param {number | string | undefined} width
92-
* @returns {number[]}
98+
* @param {string | null | undefined} sizes
99+
* @param {number[]} [deviceSizes]
100+
* @param {number[]} [imageSizes]
101+
* @returns {{ widths: number[]; kind: 'w' | 'x' }}
93102
*/
94-
function get_widths(width) {
95-
const widths = [360, 428, 720, 856, 1366, 1536, 1920, 3072, 3840];
103+
function getWidths(width, sizes, deviceSizes, imageSizes) {
96104
width = typeof width === 'string' ? parseInt(width) : width;
97-
if (typeof width === 'number') {
98-
if (width <= 300) {
99-
widths.push(width);
100-
widths.push(Math.round(width * 50) / 100);
101-
} else if (width <= 600) {
102-
widths.push(Math.round(width * 50) / 100);
103-
} else if (width > 3840) {
104-
widths.push(width);
105+
const chosen_device_sizes = deviceSizes || [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
106+
const all_sizes = (imageSizes || [16, 32, 48, 64, 96, 128, 256, 384]).concat(chosen_device_sizes);
107+
108+
if (sizes) {
109+
// Find all the "vw" percent sizes used in the sizes prop
110+
const viewport_width_re = /(^|\s)(1?\d?\d)vw/g;
111+
const percent_sizes = [];
112+
for (let match; (match = viewport_width_re.exec(sizes)); match) {
113+
percent_sizes.push(parseInt(match[2]));
114+
}
115+
if (percent_sizes.length) {
116+
const smallest_ratio = Math.min(...percent_sizes) * 0.01;
117+
return {
118+
widths: all_sizes.filter((s) => s >= chosen_device_sizes[0] * smallest_ratio),
119+
kind: 'w'
120+
};
105121
}
122+
return { widths: all_sizes, kind: 'w' };
123+
}
124+
if (typeof width !== 'number') {
125+
return { widths: chosen_device_sizes, kind: 'w' };
106126
}
107-
return widths;
127+
128+
// Don't need more than 2x resolution.
129+
// Most OLED screens that say they are 3x resolution,
130+
// are actually 3x in the green color, but only 1.5x in the red and
131+
// blue colors. Showing a 3x resolution image in the app vs a 2x
132+
// resolution image will be visually the same, though the 3x image
133+
// takes significantly more data. Even true 3x resolution screens are
134+
// wasteful as the human eye cannot see that level of detail without
135+
// something like a magnifying glass.
136+
// https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html
137+
138+
// We diverge from the Next.js logic here
139+
// You can't really scale up an image, so you can't 2x the width
140+
// Instead the user should provide the high-res image and we'll downscale
141+
// Also, Vercel builds specific image sizes and picks the closest from those,
142+
// but we can just build the ones we want exactly.
143+
return { widths: [Math.round(width / 2), width], kind: 'x' };
108144
}

packages/enhanced-img/src/preprocessor.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@ export function image(opts) {
4848

4949
let url = src_attribute.raw.trim();
5050

51+
const sizes = get_attr_value(node, 'sizes');
5152
const width = get_attr_value(node, 'width');
5253
url += url.includes('?') ? '&' : '?';
54+
if (sizes) {
55+
url += 'imgSizes=' + encodeURIComponent(sizes.raw) + '&';
56+
}
5357
if (width) {
5458
url += 'imgWidth=' + encodeURIComponent(width.raw) + '&';
5559
}
@@ -198,17 +202,15 @@ function img_to_picture(content, node, details) {
198202
/** @type {Array<import('svelte/types/compiler/interfaces').BaseDirective | import('svelte/types/compiler/interfaces').Attribute | import('svelte/types/compiler/interfaces').SpreadAttribute>} attributes */
199203
const attributes = node.attributes;
200204
const index = attributes.findIndex((attribute) => attribute.name === 'sizes');
201-
let sizes = '';
205+
let sizes_string = '';
202206
if (index >= 0) {
203-
sizes = content.substring(attributes[index].start, attributes[index].end);
207+
sizes_string = ' ' + content.substring(attributes[index].start, attributes[index].end);
204208
attributes.splice(index, 1);
205-
} else {
206-
sizes = `sizes="min(${details.image.img.w}px, 100vw)"`;
207209
}
208210

209211
let res = '<picture>';
210212
for (const [format, srcset] of Object.entries(details.image.sources)) {
211-
res += `<source srcset="${srcset}" ${sizes} type="image/${format}" />`;
213+
res += `<source srcset="${srcset}"${sizes_string} type="image/${format}" />`;
212214
}
213215
res += `<img ${img_attributes_to_markdown(content, attributes, {
214216
src: details.image.img.src,
@@ -229,12 +231,10 @@ function dynamic_img_to_picture(content, node, src_var_name) {
229231
/** @type {Array<import('svelte/types/compiler/interfaces').BaseDirective | import('svelte/types/compiler/interfaces').Attribute | import('svelte/types/compiler/interfaces').SpreadAttribute>} attributes */
230232
const attributes = node.attributes;
231233
const index = attributes.findIndex((attribute) => attribute.name === 'sizes');
232-
let sizes = '';
234+
let sizes_string = '';
233235
if (index >= 0) {
234-
sizes = '' + content.substring(attributes[index].start, attributes[index].end);
236+
sizes_string = ' ' + content.substring(attributes[index].start, attributes[index].end);
235237
attributes.splice(index, 1);
236-
} else {
237-
sizes = `sizes="min({${src_var_name}.img.w}px, 100vw)"`;
238238
}
239239

240240
const details = {
@@ -248,7 +248,7 @@ function dynamic_img_to_picture(content, node, src_var_name) {
248248
{:else}
249249
<picture>
250250
{#each Object.entries(${src_var_name}.sources) as [format, srcset]}
251-
<source {srcset} ${sizes} type={'image/' + format} />
251+
<source {srcset}${sizes_string} type={'image/' + format} />
252252
{/each}
253253
<img ${img_attributes_to_markdown(content, attributes, details)} />
254254
</picture>
Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<script lang="ts">
1+
{
2+
"code": "<script lang=\"ts\">
23
import manual_image1 from './no.png';
34
import manual_image2 from './no.svg';
45

@@ -12,41 +13,54 @@
1213

1314
{foo}
1415

15-
<img src="./foo.png" alt="non-enhanced test" />
16+
<img src=\"./foo.png\" alt=\"non-enhanced test\" />
1617

17-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 alt="basic test" width=1440 height=1440 /></picture>
18+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 alt=\"basic test\" width=1440 height=1440 /></picture>
1819

19-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 width="5" height="10" alt="dimensions test" /></picture>
20+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 width=\"5\" height=\"10\" alt=\"dimensions test\" /></picture>
2021

21-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 width=5 height=10 alt="unquoted dimensions test" /></picture>
22+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 width=5 height=10 alt=\"unquoted dimensions test\" /></picture>
2223

23-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 alt="directive test" width=1440 height=1440 /></picture>
24+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 alt=\"directive test\" width=1440 height=1440 /></picture>
2425

25-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 {...{foo}} alt="spread attributes test" width=1440 height=1440 /></picture>
26+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 {...{foo}} alt=\"spread attributes test\" width=1440 height=1440 /></picture>
2627

27-
<picture><source srcset="/1 1440w, /2 960w" sizes="(min-width: 60rem) 80vw, (min-width: 40rem) 90vw, 100vw" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="(min-width: 60rem) 80vw, (min-width: 40rem) 90vw, 100vw" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="(min-width: 60rem) 80vw, (min-width: 40rem) 90vw, 100vw" type="image/png" /><img src=/7 alt="sizes test" width=1440 height=1440 /></picture>
28+
<picture><source srcset=\"/1 1440w, /2 960w\" sizes=\"(min-width: 60rem) 80vw, (min-width: 40rem) 90vw, 100vw\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" sizes=\"(min-width: 60rem) 80vw, (min-width: 40rem) 90vw, 100vw\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" sizes=\"(min-width: 60rem) 80vw, (min-width: 40rem) 90vw, 100vw\" type=\"image/png\" /><img src=/7 alt=\"sizes test\" width=1440 height=1440 /></picture>
2829

29-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 on:click={foo = 'clicked an image!'} alt="event handler test" width=1440 height=1440 /></picture>
30+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 on:click={foo = 'clicked an image!'} alt=\"event handler test\" width=1440 height=1440 /></picture>
3031

31-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 alt="alias test" width=1440 height=1440 /></picture>
32+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 alt=\"alias test\" width=1440 height=1440 /></picture>
3233

33-
<picture><source srcset="/1 1440w, /2 960w" sizes="min(1440px, 100vw)" type="image/avif" /><source srcset="/3 1440w, /4 960w" sizes="min(1440px, 100vw)" type="image/webp" /><source srcset="5 1440w, /6 960w" sizes="min(1440px, 100vw)" type="image/png" /><img src=/7 alt="absolute path test" width=1440 height=1440 /></picture>
34+
<picture><source srcset=\"/1 1440w, /2 960w\" type=\"image/avif\" /><source srcset=\"/3 1440w, /4 960w\" type=\"image/webp\" /><source srcset=\"5 1440w, /6 960w\" type=\"image/png\" /><img src=/7 alt=\"absolute path test\" width=1440 height=1440 /></picture>
3435

3536
{#each images as image}
3637
{#if typeof image === 'string'}
37-
<img src={image.img.src} alt="opt-in test" width={image.img.w} height={image.img.h} />
38+
<img src={image.img.src} alt=\"opt-in test\" width={image.img.w} height={image.img.h} />
3839
{:else}
3940
<picture>
4041
{#each Object.entries(image.sources) as [format, srcset]}
41-
<source {srcset} sizes="min({image.img.w}px, 100vw)" type={'image/' + format} />
42+
<source {srcset} type={'image/' + format} />
4243
{/each}
43-
<img src={image.img.src} alt="opt-in test" width={image.img.w} height={image.img.h} />
44+
<img src={image.img.src} alt=\"opt-in test\" width={image.img.w} height={image.img.h} />
4445
</picture>
4546
{/if}
4647
{/each}
4748

4849
<picture>
49-
<source src="./foo.avif" />
50-
<source srcset="./foo.avif 500v ./bar.avif 100v" />
51-
<source srcset="./foo.avif, ./bar.avif 1v" />
50+
<source src=\"./foo.avif\" />
51+
<source srcset=\"./foo.avif 500v ./bar.avif 100v\" />
52+
<source srcset=\"./foo.avif, ./bar.avif 1v\" />
5253
</picture>
54+
",
55+
"dependencies": [],
56+
"map": SourceMap {
57+
"mappings": "",
58+
"names": [],
59+
"sourceRoot": undefined,
60+
"sources": [
61+
"Input.svelte",
62+
],
63+
"version": 3,
64+
},
65+
"toString": [Function],
66+
}

packages/enhanced-img/test/preprocessor.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ it('Image preprocess snapshot test', async () => {
2929
{ filename }
3030
);
3131

32-
expect(processed.code).toMatchFileSnapshot('./Output.svelte');
32+
expect(processed).toMatchFileSnapshot('./Output.svelte');
3333
});
3434

3535
it('parses a minimized object', () => {

0 commit comments

Comments
 (0)