Skip to content

Commit a85f441

Browse files
authored
feat(next/image): add support for images.qualities in next.config (#74500)
Backports PR #74257 to 14.x
1 parent adfe537 commit a85f441

File tree

24 files changed

+395
-9
lines changed

24 files changed

+395
-9
lines changed

docs/02-app/02-api-reference/01-components/image.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ quality={75} // {number 1-100}
247247

248248
The quality of the optimized image, an integer between `1` and `100`, where `100` is the best quality and therefore largest file size. Defaults to `75`.
249249

250+
If the [`qualities`](#qualities) configuration is defined in `next.config.js`, the `quality` prop must match one of the values defined in the configuration.
251+
252+
> **Good to know**: If the original source image was already low quality, setting the quality prop too high could cause the resulting optimized image to be larger than the original source image.
253+
250254
### `priority`
251255

252256
```js
@@ -672,6 +676,20 @@ module.exports = {
672676
}
673677
```
674678

679+
### `qualities`
680+
681+
The default [Image Optimization API](#loader) will automatically allow all qualities from 1 to 100. If you wish to restrict the allowed qualities, you can add configuration to `next.config.js`.
682+
683+
```js filename="next.config.js"
684+
module.exports = {
685+
images: {
686+
qualities: [25, 50, 75],
687+
},
688+
}
689+
```
690+
691+
In this example above, only three qualities are allowed: 25, 50, and 75. If the [`quality`](#quality) prop does not match a value in this array, the image will fail with 400 Bad Request.
692+
675693
### `formats`
676694

677695
The default [Image Optimization API](#loader) will automatically detect the browser's supported image formats via the request's `Accept` header.
@@ -1050,6 +1068,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c
10501068

10511069
| Version | Changes |
10521070
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1071+
| `v14.2.23` | `qualities` configuration added. |
10531072
| `v14.2.15` | `decoding` prop added and `localPatterns` configuration added. |
10541073
| `v14.2.14` | `remotePatterns.search` prop added. |
10551074
| `v14.2.0` | `overrideSrc` prop added. |

errors/invalid-images-config.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ module.exports = {
4141
localPatterns: [],
4242
// limit of 50 objects
4343
remotePatterns: [],
44+
// limit of 20 integers
45+
qualities: [25, 50, 75],
4446
// when true, every image will be unoptimized
4547
unoptimized: false,
4648
},
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
title: '`next/image` Un-configured qualities'
3+
---
4+
5+
## Why This Error Occurred
6+
7+
One of your pages that leverages the `next/image` component, passed a `quality` value that isn't defined in the `images.qualities` property in `next.config.js`.
8+
9+
## Possible Ways to Fix It
10+
11+
Add an entry to `images.qualities` array in `next.config.js` with the expected value. For example:
12+
13+
```js filename="next.config.js"
14+
module.exports = {
15+
images: {
16+
qualities: [25, 50, 75],
17+
},
18+
}
19+
```
20+
21+
## Useful Links
22+
23+
- [Image Optimization Documentation](/docs/pages/building-your-application/optimizing/images)
24+
- [Qualities Config Documentation](/docs/pages/api-reference/components/image#qualities)

packages/next/src/build/webpack/plugins/define-env-plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,14 @@ function getImageConfig(
108108
'process.env.__NEXT_IMAGE_OPTS': {
109109
deviceSizes: config.images.deviceSizes,
110110
imageSizes: config.images.imageSizes,
111+
qualities: config.images.qualities,
111112
path: config.images.path,
112113
loader: config.images.loader,
113114
dangerouslyAllowSVG: config.images.dangerouslyAllowSVG,
114115
unoptimized: config?.images?.unoptimized,
115116
...(dev
116117
? {
117-
// pass domains in development to allow validating on the client
118+
// additional config in dev to allow validating on the client
118119
domains: config.images.domains,
119120
remotePatterns: config.images?.remotePatterns,
120121
localPatterns: config.images?.localPatterns,

packages/next/src/client/image-component.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ export const Image = forwardRef<HTMLImageElement | null, ImageProps>(
374374
const c = configEnv || configContext || imageConfigDefault
375375
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
376376
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
377-
return { ...c, allSizes, deviceSizes }
377+
const qualities = c.qualities?.sort((a, b) => a - b)
378+
return { ...c, allSizes, deviceSizes, qualities }
378379
}, [configContext])
379380

380381
const { onLoad, onLoadingComplete } = props

packages/next/src/client/legacy/image.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { normalizePathTrailingSlash } from '../normalize-trailing-slash'
2525
function normalizeSrc(src: string): string {
2626
return src[0] === '/' ? src.slice(1) : src
2727
}
28-
28+
const DEFAULT_Q = 75
2929
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
3030
const loadedImageURLs = new Set<string>()
3131
const allImgs = new Map<
@@ -186,8 +186,22 @@ function defaultLoader({
186186
}
187187
}
188188
}
189+
190+
if (quality && config.qualities && !config.qualities.includes(quality)) {
191+
throw new Error(
192+
`Invalid quality prop (${quality}) on \`next/image\` does not match \`images.qualities\` configured in your \`next.config.js\`\n` +
193+
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities`
194+
)
195+
}
189196
}
190197

198+
const q =
199+
quality ||
200+
config.qualities?.reduce((prev, cur) =>
201+
Math.abs(cur - DEFAULT_Q) < Math.abs(prev - DEFAULT_Q) ? cur : prev
202+
) ||
203+
DEFAULT_Q
204+
191205
if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) {
192206
// Special case to make svg serve as-is to avoid proxying
193207
// through the built-in Image Optimization API.
@@ -196,7 +210,7 @@ function defaultLoader({
196210

197211
return `${normalizePathTrailingSlash(config.path)}?url=${encodeURIComponent(
198212
src
199-
)}&w=${width}&q=${quality || 75}`
213+
)}&w=${width}&q=${q}`
200214
}
201215

202216
const loaders = new Map<
@@ -637,7 +651,8 @@ export default function Image({
637651
const c = configEnv || configContext || imageConfigDefault
638652
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
639653
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
640-
return { ...c, allSizes, deviceSizes }
654+
const qualities = c.qualities?.sort((a, b) => a - b)
655+
return { ...c, allSizes, deviceSizes, qualities }
641656
}, [configContext])
642657

643658
let rest: Partial<ImageProps> = all

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,11 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
505505
loaderFile: z.string().optional(),
506506
minimumCacheTTL: z.number().int().gte(0).optional(),
507507
path: z.string().optional(),
508+
qualities: z
509+
.array(z.number().int().gte(1).lte(100))
510+
.min(1)
511+
.max(20)
512+
.optional(),
508513
})
509514
.optional(),
510515
logging: z

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export class ImageOptimizerCache {
175175
} = imageData
176176
const remotePatterns = nextConfig.images?.remotePatterns || []
177177
const localPatterns = nextConfig.images?.localPatterns
178+
const qualities = nextConfig.images?.qualities
178179
const { url, w, q } = query
179180
let href: string
180181

@@ -281,6 +282,18 @@ export class ImageOptimizerCache {
281282
}
282283
}
283284

285+
if (qualities) {
286+
if (isDev) {
287+
qualities.push(BLUR_QUALITY)
288+
}
289+
290+
if (!qualities.includes(quality)) {
291+
return {
292+
errorMessage: `"q" parameter (quality) of ${q} is not allowed`,
293+
}
294+
}
295+
}
296+
284297
const mimeType = getSupportedMimeType(formats || [], req.headers['accept'])
285298

286299
const isStatic = url.startsWith(

packages/next/src/shared/lib/get-img-props.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ export function getImgProps(
283283
} else {
284284
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
285285
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
286-
config = { ...c, allSizes, deviceSizes }
286+
const qualities = c.qualities?.sort((a, b) => a - b)
287+
config = { ...c, allSizes, deviceSizes, qualities }
287288
}
288289

289290
if (typeof defaultLoader === 'undefined') {

packages/next/src/shared/lib/image-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export type ImageConfigComplete = {
118118
/** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#localPatterns) */
119119
localPatterns: LocalPattern[] | undefined
120120

121+
/** @see [Qualities](https://nextjs.org/docs/api-reference/next/image#qualities) */
122+
qualities: number[] | undefined
123+
121124
/** @see [Unoptimized](https://nextjs.org/docs/api-reference/next/image#unoptimized) */
122125
unoptimized: boolean
123126
}
@@ -139,5 +142,6 @@ export const imageConfigDefault: ImageConfigComplete = {
139142
contentDispositionType: 'inline',
140143
localPatterns: undefined, // default: allow all local images
141144
remotePatterns: [], // default: allow no remote images
145+
qualities: undefined, // default: allow all qualities
142146
unoptimized: false,
143147
}

0 commit comments

Comments
 (0)