diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md
index f88194d86bb48ab..fbe1ab86c60c08b 100644
--- a/docs/basic-features/image-optimization.md
+++ b/docs/basic-features/image-optimization.md
@@ -50,14 +50,26 @@ export default Home
You can configure Image Optimization by using the `images` property in `next.config.js`.
-### Sizes
+### Device Sizes
-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.
+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.
```js
module.exports = {
images: {
- sizes: [320, 420, 768, 1024, 1200],
+ deviceSizes: [320, 420, 768, 1024, 1200],
+ },
+}
+```
+
+### Icon Sizes
+
+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.
+
+```js
+module.exports = {
+ images: {
+ iconSizes: [16, 32, 64],
},
}
```
@@ -96,7 +108,6 @@ The following Image Optimization cloud providers are supported:
- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'`
- [Akamai](https://www.akamai.com): `loader: 'akamai'`
-
## Related
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:
See all available properties for the Image component
-
diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts
index 81256b9875275d4..67cdd21d3b35373 100644
--- a/packages/next/build/index.ts
+++ b/packages/next/build/index.ts
@@ -1110,11 +1110,15 @@ export default async function build(
)
}
+ const images = { ...config.images }
+ const { deviceSizes, iconSizes } = images
+ images.sizes = [...deviceSizes, ...iconSizes]
+
await promises.writeFile(
path.join(distDir, IMAGES_MANIFEST),
JSON.stringify({
version: 1,
- images: config.images,
+ images,
}),
'utf8'
)
diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index d174521b7ad9ad5..3559fb5f955c5e6 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -991,7 +991,8 @@ export default async function getBaseWebpackConfig(
config.experimental.scrollRestoration
),
'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({
- sizes: config.images.sizes,
+ deviceSizes: config.images.deviceSizes,
+ iconSizes: config.images.iconSizes,
path: config.images.path,
loader: config.images.loader,
...(dev
diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx
index 457b2211d1869ef..d938348a338aa59 100644
--- a/packages/next/client/image.tsx
+++ b/packages/next/client/image.tsx
@@ -14,7 +14,8 @@ const loaders = new Map string>([
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default'
type ImageData = {
- sizes: number[]
+ deviceSizes: number[]
+ iconSizes: number[]
loader: LoaderKey
path: string
domains?: string[]
@@ -36,12 +37,15 @@ type ImageProps = Omit<
const imageData: ImageData = process.env.__NEXT_IMAGE_OPTS as any
const {
- sizes: configSizes,
+ deviceSizes: configDeviceSizes,
+ iconSizes: configIconSizes,
loader: configLoader,
path: configPath,
domains: configDomains,
} = imageData
-configSizes.sort((a, b) => a - b) // smallest to largest
+// sort smallest to largest
+configDeviceSizes.sort((a, b) => a - b)
+configIconSizes.sort((a, b) => a - b)
let cachedObserver: IntersectionObserver
const IntersectionObserver =
@@ -79,12 +83,16 @@ function getObserver(): IntersectionObserver | undefined {
))
}
-function getWidthsFromConfig(width: number | undefined) {
+function getDeviceSizes(width: number | undefined): number[] {
if (typeof width !== 'number') {
- return configSizes
+ return configDeviceSizes
+ }
+ const smallest = configDeviceSizes[0]
+ if (width < smallest && configIconSizes.includes(width)) {
+ return [width]
}
const widths: number[] = []
- for (let size of configSizes) {
+ for (let size of configDeviceSizes) {
widths.push(size)
if (size >= width) {
break
@@ -102,7 +110,7 @@ function computeSrc(
if (unoptimized) {
return src
}
- const widths = getWidthsFromConfig(width)
+ const widths = getDeviceSizes(width)
const largest = widths[widths.length - 1]
return callLoader({ src, width: largest, quality })
}
@@ -136,7 +144,8 @@ function generateSrcSet({
if (unoptimized) {
return undefined
}
- return getWidthsFromConfig(width)
+
+ return getDeviceSizes(width)
.map((w) => `${callLoader({ src, width: w, quality })} ${w}w`)
.join(', ')
}
diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts
index a489544e3b8ea76..1940286b801c18b 100644
--- a/packages/next/next-server/server/config.ts
+++ b/packages/next/next-server/server/config.ts
@@ -24,7 +24,8 @@ const defaultConfig: { [key: string]: any } = {
poweredByHeader: true,
compress: true,
images: {
- sizes: [320, 420, 768, 1024, 1200],
+ deviceSizes: [320, 420, 768, 1024, 1200],
+ iconSizes: [],
domains: [],
path: '/_next/image',
loader: 'default',
@@ -253,26 +254,53 @@ function assignDefaults(userConfig: { [key: string]: any }) {
)
}
}
- if (images.sizes) {
- if (!Array.isArray(images.sizes)) {
+ if (images.deviceSizes) {
+ const { deviceSizes } = images
+ if (!Array.isArray(deviceSizes)) {
throw new Error(
- `Specified images.sizes should be an Array received ${typeof images.sizes}`
+ `Specified images.deviceSizes should be an Array received ${typeof deviceSizes}`
)
}
- if (images.sizes.length > 50) {
+ if (deviceSizes.length > 25) {
throw new Error(
- `Specified images.sizes exceeds length of 50, received length (${images.sizes.length}), please reduce the length of the array to continue`
+ `Specified images.deviceSizes exceeds length of 25, received length (${deviceSizes.length}), please reduce the length of the array to continue`
)
}
- const invalid = images.sizes.filter((d: unknown) => {
+ const invalid = deviceSizes.filter((d: unknown) => {
return typeof d !== 'number' || d < 1 || d > 10000
})
if (invalid.length > 0) {
throw new Error(
- `Specified images.sizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
+ `Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
+ ', '
+ )})`
+ )
+ }
+ }
+ if (images.iconSizes) {
+ const { iconSizes } = images
+ if (!Array.isArray(iconSizes)) {
+ throw new Error(
+ `Specified images.iconSizes should be an Array received ${typeof iconSizes}`
+ )
+ }
+
+ if (iconSizes.length > 25) {
+ throw new Error(
+ `Specified images.iconSizes exceeds length of 25, received length (${iconSizes.length}), please reduce the length of the array to continue`
+ )
+ }
+
+ const invalid = iconSizes.filter((d: unknown) => {
+ return typeof d !== 'number' || d < 1 || d > 10000
+ })
+
+ if (invalid.length > 0) {
+ throw new Error(
+ `Specified images.iconSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
', '
)})`
)
diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts
index c0c2e023d7b01fc..d1825f7b2f5e6d1 100644
--- a/packages/next/next-server/server/image-optimizer.ts
+++ b/packages/next/next-server/server/image-optimizer.ts
@@ -23,6 +23,14 @@ const CACHE_VERSION = 1
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
const VECTOR_TYPES = [SVG]
+type ImageData = {
+ deviceSizes: number[]
+ iconSizes: number[]
+ loader: string
+ path: string
+ domains?: string[]
+}
+
export async function imageOptimizer(
server: Server,
req: IncomingMessage,
@@ -30,7 +38,9 @@ export async function imageOptimizer(
parsedUrl: UrlWithParsedQuery
) {
const { nextConfig, distDir } = server
- const { sizes = [], domains = [], loader } = nextConfig?.images || {}
+ const imageData: ImageData = nextConfig.images
+ const { deviceSizes = [], iconSizes = [], domains = [], loader } = imageData
+ const sizes = [...deviceSizes, ...iconSizes]
if (loader !== 'default') {
await server.render404(req, res, parsedUrl)
diff --git a/test/integration/image-component/basic/next.config.js b/test/integration/image-component/basic/next.config.js
index 6adf797d1c2b1e5..617de0a68d69b42 100644
--- a/test/integration/image-component/basic/next.config.js
+++ b/test/integration/image-component/basic/next.config.js
@@ -1,6 +1,7 @@
module.exports = {
images: {
- sizes: [480, 1024, 1600, 2000],
+ deviceSizes: [480, 1024, 1600, 2000],
+ iconSizes: [16, 64],
path: 'https://example.com/myaccount/',
loader: 'imgix',
},
diff --git a/test/integration/image-component/basic/pages/client-side.js b/test/integration/image-component/basic/pages/client-side.js
index 99f97951f2bdbd4..ddd00fd2fe3e9d5 100644
--- a/test/integration/image-component/basic/pages/client-side.js
+++ b/test/integration/image-component/basic/pages/client-side.js
@@ -53,6 +53,20 @@ const ClientSide = () => {
width={300}
height={400}
/>
+
+
Errors
diff --git a/test/integration/image-component/basic/pages/index.js b/test/integration/image-component/basic/pages/index.js
index a930e0ef1ef8019..c08992abfb47602 100644
--- a/test/integration/image-component/basic/pages/index.js
+++ b/test/integration/image-component/basic/pages/index.js
@@ -70,6 +70,20 @@ const Page = () => {
width={300}
height={400}
/>
+
+
Client Side
diff --git a/test/integration/image-component/basic/test/index.test.js b/test/integration/image-component/basic/test/index.test.js
index 9e9198cdac42b0f..ff369bda69b8d1f 100644
--- a/test/integration/image-component/basic/test/index.test.js
+++ b/test/integration/image-component/basic/test/index.test.js
@@ -51,6 +51,20 @@ function runTests() {
await browser.elementById('preceding-slash-image').getAttribute('srcset')
).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480 480w')
})
+ it('should use iconSizes when width matches, not deviceSizes from next.config.js', async () => {
+ expect(await browser.elementById('icon-image-16').getAttribute('src')).toBe(
+ 'https://example.com/myaccount/icon.png?auto=format&w=16'
+ )
+ expect(
+ await browser.elementById('icon-image-16').getAttribute('srcset')
+ ).toBe('https://example.com/myaccount/icon.png?auto=format&w=16 16w')
+ expect(await browser.elementById('icon-image-64').getAttribute('src')).toBe(
+ 'https://example.com/myaccount/icon.png?auto=format&w=64'
+ )
+ expect(
+ await browser.elementById('icon-image-64').getAttribute('srcset')
+ ).toBe('https://example.com/myaccount/icon.png?auto=format&w=64 64w')
+ })
it('should support the unoptimized attribute', async () => {
expect(
await browser.elementById('unoptimized-image').getAttribute('src')
diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js
index 8f457d326a6b15f..3428ffd359b08c8 100644
--- a/test/integration/image-optimizer/test/index.test.js
+++ b/test/integration/image-optimizer/test/index.test.js
@@ -348,12 +348,12 @@ describe('Image Optimizer', () => {
)
})
- it('should error when sizes length exceeds 50', async () => {
+ it('should error when sizes length exceeds 25', async () => {
await nextConfig.replace(
'{ /* replaceme */ }',
JSON.stringify({
images: {
- sizes: new Array(51).fill(1024),
+ deviceSizes: new Array(51).fill(1024),
},
})
)
@@ -369,16 +369,16 @@ describe('Image Optimizer', () => {
await nextConfig.restore()
expect(stderr).toContain(
- 'Specified images.sizes exceeds length of 50, received length (51), please reduce the length of the array to continue'
+ 'Specified images.deviceSizes exceeds length of 25, received length (51), please reduce the length of the array to continue'
)
})
- it('should error when sizes contains invalid sizes', async () => {
+ it('should error when deviceSizes contains invalid widths', async () => {
await nextConfig.replace(
'{ /* replaceme */ }',
JSON.stringify({
images: {
- sizes: [0, 12000, 64, 128, 256],
+ deviceSizes: [0, 12000, 64, 128, 256],
},
})
)
@@ -394,7 +394,32 @@ describe('Image Optimizer', () => {
await nextConfig.restore()
expect(stderr).toContain(
- 'Specified images.sizes should be an Array of numbers that are between 1 and 10000, received invalid values (0, 12000)'
+ 'Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (0, 12000)'
+ )
+ })
+
+ it('should error when iconSizes contains invalid widths', async () => {
+ await nextConfig.replace(
+ '{ /* replaceme */ }',
+ JSON.stringify({
+ images: {
+ iconSizes: [0, 16, 64, 12000],
+ },
+ })
+ )
+ let stderr = ''
+
+ app = await launchApp(appDir, await findPort(), {
+ onStderr(msg) {
+ stderr += msg || ''
+ },
+ })
+ await waitFor(1000)
+ await killApp(app).catch(() => {})
+ await nextConfig.restore()
+
+ expect(stderr).toContain(
+ 'Specified images.iconSizes should be an Array of numbers that are between 1 and 10000, received invalid values (0, 12000)'
)
})
})
@@ -421,7 +446,8 @@ describe('Image Optimizer', () => {
beforeAll(async () => {
const json = JSON.stringify({
images: {
- sizes: [size, largeSize],
+ deviceSizes: [largeSize],
+ iconSizes: [size],
domains,
},
})
@@ -458,7 +484,7 @@ describe('Image Optimizer', () => {
beforeAll(async () => {
const json = JSON.stringify({
images: {
- sizes: [size, largeSize],
+ deviceSizes: [size, largeSize],
domains,
},
})
@@ -482,7 +508,7 @@ describe('Image Optimizer', () => {
const json = JSON.stringify({
target: 'experimental-serverless-trace',
images: {
- sizes: [size, largeSize],
+ deviceSizes: [size, largeSize],
domains,
},
})