Skip to content

Commit 3a169fb

Browse files
authored
Separate config into deviceSizes and iconSizes (#18267)
This separates the `next.config.js` property `images.sizes` into to properties: `images.deviceSizes` and `images.iconSizes`. The purpose is for images that are not intended to take up the majority of the viewport. Related to #18122
1 parent 557a000 commit 3a169fb

File tree

11 files changed

+165
-34
lines changed

11 files changed

+165
-34
lines changed

docs/basic-features/image-optimization.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,26 @@ export default Home
5050

5151
You can configure Image Optimization by using the `images` property in `next.config.js`.
5252

53-
### Sizes
53+
### Device Sizes
5454

55-
You can specify a list of image widths to allow using the `sizes` property. Since images maintain their aspect ratio using the `width` and `height` attributes of the source image, there is no need to specify height in `next.config.js` – only the width. You can think of these as breakpoints.
55+
You can specify a list of device width breakpoints using the `deviceSizes` property. Since images maintain their aspect ratio using the `width` and `height` attributes of the source image, there is no need to specify height in `next.config.js` – only the width. These values will be used by the browser to determine which size image should load.
5656

5757
```js
5858
module.exports = {
5959
images: {
60-
sizes: [320, 420, 768, 1024, 1200],
60+
deviceSizes: [320, 420, 768, 1024, 1200],
61+
},
62+
}
63+
```
64+
65+
### Icon Sizes
66+
67+
You can specify a list of icon image widths using the `iconSizes` property. These widths should be smaller than the smallest value in `deviceSizes`. The purpose is for images that don't scale with the browser window, such as icons or badges. If `iconSizes` is not defined, then `deviceSizes` will be used.
68+
69+
```js
70+
module.exports = {
71+
images: {
72+
iconSizes: [16, 32, 64],
6173
},
6274
}
6375
```
@@ -96,7 +108,6 @@ The following Image Optimization cloud providers are supported:
96108
- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'`
97109
- [Akamai](https://www.akamai.com): `loader: 'akamai'`
98110

99-
100111
## Related
101112

102113
For more information on what to do next, we recommend the following sections:
@@ -107,4 +118,3 @@ For more information on what to do next, we recommend the following sections:
107118
<small>See all available properties for the Image component</small>
108119
</a>
109120
</div>
110-

packages/next/build/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,11 +1110,15 @@ export default async function build(
11101110
)
11111111
}
11121112

1113+
const images = { ...config.images }
1114+
const { deviceSizes, iconSizes } = images
1115+
images.sizes = [...deviceSizes, ...iconSizes]
1116+
11131117
await promises.writeFile(
11141118
path.join(distDir, IMAGES_MANIFEST),
11151119
JSON.stringify({
11161120
version: 1,
1117-
images: config.images,
1121+
images,
11181122
}),
11191123
'utf8'
11201124
)

packages/next/build/webpack-config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,8 @@ export default async function getBaseWebpackConfig(
991991
config.experimental.scrollRestoration
992992
),
993993
'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({
994-
sizes: config.images.sizes,
994+
deviceSizes: config.images.deviceSizes,
995+
iconSizes: config.images.iconSizes,
995996
path: config.images.path,
996997
loader: config.images.loader,
997998
...(dev

packages/next/client/image.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const loaders = new Map<LoaderKey, (props: LoaderProps) => string>([
1414
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default'
1515

1616
type ImageData = {
17-
sizes: number[]
17+
deviceSizes: number[]
18+
iconSizes: number[]
1819
loader: LoaderKey
1920
path: string
2021
domains?: string[]
@@ -36,12 +37,15 @@ type ImageProps = Omit<
3637

3738
const imageData: ImageData = process.env.__NEXT_IMAGE_OPTS as any
3839
const {
39-
sizes: configSizes,
40+
deviceSizes: configDeviceSizes,
41+
iconSizes: configIconSizes,
4042
loader: configLoader,
4143
path: configPath,
4244
domains: configDomains,
4345
} = imageData
44-
configSizes.sort((a, b) => a - b) // smallest to largest
46+
// sort smallest to largest
47+
configDeviceSizes.sort((a, b) => a - b)
48+
configIconSizes.sort((a, b) => a - b)
4549

4650
let cachedObserver: IntersectionObserver
4751
const IntersectionObserver =
@@ -79,12 +83,16 @@ function getObserver(): IntersectionObserver | undefined {
7983
))
8084
}
8185

82-
function getWidthsFromConfig(width: number | undefined) {
86+
function getDeviceSizes(width: number | undefined): number[] {
8387
if (typeof width !== 'number') {
84-
return configSizes
88+
return configDeviceSizes
89+
}
90+
const smallest = configDeviceSizes[0]
91+
if (width < smallest && configIconSizes.includes(width)) {
92+
return [width]
8593
}
8694
const widths: number[] = []
87-
for (let size of configSizes) {
95+
for (let size of configDeviceSizes) {
8896
widths.push(size)
8997
if (size >= width) {
9098
break
@@ -102,7 +110,7 @@ function computeSrc(
102110
if (unoptimized) {
103111
return src
104112
}
105-
const widths = getWidthsFromConfig(width)
113+
const widths = getDeviceSizes(width)
106114
const largest = widths[widths.length - 1]
107115
return callLoader({ src, width: largest, quality })
108116
}
@@ -136,7 +144,8 @@ function generateSrcSet({
136144
if (unoptimized) {
137145
return undefined
138146
}
139-
return getWidthsFromConfig(width)
147+
148+
return getDeviceSizes(width)
140149
.map((w) => `${callLoader({ src, width: w, quality })} ${w}w`)
141150
.join(', ')
142151
}

packages/next/next-server/server/config.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ const defaultConfig: { [key: string]: any } = {
2424
poweredByHeader: true,
2525
compress: true,
2626
images: {
27-
sizes: [320, 420, 768, 1024, 1200],
27+
deviceSizes: [320, 420, 768, 1024, 1200],
28+
iconSizes: [],
2829
domains: [],
2930
path: '/_next/image',
3031
loader: 'default',
@@ -253,26 +254,53 @@ function assignDefaults(userConfig: { [key: string]: any }) {
253254
)
254255
}
255256
}
256-
if (images.sizes) {
257-
if (!Array.isArray(images.sizes)) {
257+
if (images.deviceSizes) {
258+
const { deviceSizes } = images
259+
if (!Array.isArray(deviceSizes)) {
258260
throw new Error(
259-
`Specified images.sizes should be an Array received ${typeof images.sizes}`
261+
`Specified images.deviceSizes should be an Array received ${typeof deviceSizes}`
260262
)
261263
}
262264

263-
if (images.sizes.length > 50) {
265+
if (deviceSizes.length > 25) {
264266
throw new Error(
265-
`Specified images.sizes exceeds length of 50, received length (${images.sizes.length}), please reduce the length of the array to continue`
267+
`Specified images.deviceSizes exceeds length of 25, received length (${deviceSizes.length}), please reduce the length of the array to continue`
266268
)
267269
}
268270

269-
const invalid = images.sizes.filter((d: unknown) => {
271+
const invalid = deviceSizes.filter((d: unknown) => {
270272
return typeof d !== 'number' || d < 1 || d > 10000
271273
})
272274

273275
if (invalid.length > 0) {
274276
throw new Error(
275-
`Specified images.sizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
277+
`Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
278+
', '
279+
)})`
280+
)
281+
}
282+
}
283+
if (images.iconSizes) {
284+
const { iconSizes } = images
285+
if (!Array.isArray(iconSizes)) {
286+
throw new Error(
287+
`Specified images.iconSizes should be an Array received ${typeof iconSizes}`
288+
)
289+
}
290+
291+
if (iconSizes.length > 25) {
292+
throw new Error(
293+
`Specified images.iconSizes exceeds length of 25, received length (${iconSizes.length}), please reduce the length of the array to continue`
294+
)
295+
}
296+
297+
const invalid = iconSizes.filter((d: unknown) => {
298+
return typeof d !== 'number' || d < 1 || d > 10000
299+
})
300+
301+
if (invalid.length > 0) {
302+
throw new Error(
303+
`Specified images.iconSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
276304
', '
277305
)})`
278306
)

packages/next/next-server/server/image-optimizer.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,24 @@ const CACHE_VERSION = 1
2323
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
2424
const VECTOR_TYPES = [SVG]
2525

26+
type ImageData = {
27+
deviceSizes: number[]
28+
iconSizes: number[]
29+
loader: string
30+
path: string
31+
domains?: string[]
32+
}
33+
2634
export async function imageOptimizer(
2735
server: Server,
2836
req: IncomingMessage,
2937
res: ServerResponse,
3038
parsedUrl: UrlWithParsedQuery
3139
) {
3240
const { nextConfig, distDir } = server
33-
const { sizes = [], domains = [], loader } = nextConfig?.images || {}
41+
const imageData: ImageData = nextConfig.images
42+
const { deviceSizes = [], iconSizes = [], domains = [], loader } = imageData
43+
const sizes = [...deviceSizes, ...iconSizes]
3444

3545
if (loader !== 'default') {
3646
await server.render404(req, res, parsedUrl)

test/integration/image-component/basic/next.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
images: {
3-
sizes: [480, 1024, 1600, 2000],
3+
deviceSizes: [480, 1024, 1600, 2000],
4+
iconSizes: [16, 64],
45
path: 'https://example.com/myaccount/',
56
loader: 'imgix',
67
},

test/integration/image-component/basic/pages/client-side.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ const ClientSide = () => {
5353
width={300}
5454
height={400}
5555
/>
56+
<Image
57+
id="icon-image-64"
58+
src="/icon.png"
59+
loading="eager"
60+
width={64}
61+
height={64}
62+
/>
63+
<Image
64+
id="icon-image-16"
65+
src="/icon.png"
66+
loading="eager"
67+
width={16}
68+
height={16}
69+
/>
5670
<Link href="/errors">
5771
<a id="errorslink">Errors</a>
5872
</Link>

test/integration/image-component/basic/pages/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ const Page = () => {
7070
width={300}
7171
height={400}
7272
/>
73+
<Image
74+
id="icon-image-64"
75+
src="/icon.png"
76+
loading="eager"
77+
width={64}
78+
height={64}
79+
/>
80+
<Image
81+
id="icon-image-16"
82+
src="/icon.png"
83+
loading="eager"
84+
width={16}
85+
height={16}
86+
/>
7387
<Link href="/client-side">
7488
<a id="clientlink">Client Side</a>
7589
</Link>

test/integration/image-component/basic/test/index.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ function runTests() {
5151
await browser.elementById('preceding-slash-image').getAttribute('srcset')
5252
).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480 480w')
5353
})
54+
it('should use iconSizes when width matches, not deviceSizes from next.config.js', async () => {
55+
expect(await browser.elementById('icon-image-16').getAttribute('src')).toBe(
56+
'https://example.com/myaccount/icon.png?auto=format&w=16'
57+
)
58+
expect(
59+
await browser.elementById('icon-image-16').getAttribute('srcset')
60+
).toBe('https://example.com/myaccount/icon.png?auto=format&w=16 16w')
61+
expect(await browser.elementById('icon-image-64').getAttribute('src')).toBe(
62+
'https://example.com/myaccount/icon.png?auto=format&w=64'
63+
)
64+
expect(
65+
await browser.elementById('icon-image-64').getAttribute('srcset')
66+
).toBe('https://example.com/myaccount/icon.png?auto=format&w=64 64w')
67+
})
5468
it('should support the unoptimized attribute', async () => {
5569
expect(
5670
await browser.elementById('unoptimized-image').getAttribute('src')

0 commit comments

Comments
 (0)