Skip to content

Commit d70595e

Browse files
authored
feat(next/image): add support for images.qualities in next.config (vercel#74257)
This PR adds support for `images.qualities` configuration which is an allowlist of qualities that can be used with the quality prop on the Image component. - Depends on vercel/vercel#12792
1 parent 0e4cff4 commit d70595e

File tree

25 files changed

+397
-10
lines changed

25 files changed

+397
-10
lines changed

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

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

258258
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`.
259259

260+
If the [`qualities`](#qualities) configuration is defined in `next.config.js`, the `quality` prop must match one of the values defined in the configuration.
261+
262+
> **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.
263+
260264
### `priority`
261265

262266
```js
@@ -681,6 +685,20 @@ module.exports = {
681685
}
682686
```
683687

688+
### `qualities`
689+
690+
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`.
691+
692+
```js filename="next.config.js"
693+
module.exports = {
694+
images: {
695+
qualities: [25, 50, 75],
696+
},
697+
}
698+
```
699+
700+
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.
701+
684702
### `formats`
685703

686704
The default [Image Optimization API](#loader) will automatically detect the browser's supported image formats via the request's `Accept` header in order to determine the best output format.
@@ -1076,6 +1094,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c
10761094
| Version | Changes |
10771095
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10781096
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |
1097+
| `v14.2.23` | `qualities` configuration added. |
10791098
| `v14.2.15` | `decoding` prop added and `localPatterns` configuration added. |
10801099
| `v14.2.14` | `remotePatterns.search` prop added. |
10811100
| `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/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,5 +620,6 @@
620620
"619": "Page not found",
621621
"620": "A required parameter (%s) was not provided as %s received %s in getStaticPaths for %s",
622622
"621": "Required root params (%s) were not provided in generateStaticParams for %s, please provide at least one value for each.",
623-
"622": "A required root parameter (%s) was not provided in generateStaticParams for %s, please provide at least one value."
623+
"622": "A required root parameter (%s) was not provided in generateStaticParams for %s, please provide at least one value.",
624+
"623": "Invalid quality prop (%s) on \\`next/image\\` does not match \\`images.qualities\\` configured in your \\`next.config.js\\`\\nSee more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities"
624625
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,14 @@ function getImageConfig(
109109
'process.env.__NEXT_IMAGE_OPTS': {
110110
deviceSizes: config.images.deviceSizes,
111111
imageSizes: config.images.imageSizes,
112+
qualities: config.images.qualities,
112113
path: config.images.path,
113114
loader: config.images.loader,
114115
dangerouslyAllowSVG: config.images.dangerouslyAllowSVG,
115116
unoptimized: config?.images?.unoptimized,
116117
...(dev
117118
? {
118-
// pass domains in development to allow validating on the client
119+
// additional config in dev to allow validating on the client
119120
domains: config.images.domains,
120121
remotePatterns: config.images?.remotePatterns,
121122
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
@@ -371,7 +371,8 @@ export const Image = forwardRef<HTMLImageElement | null, ImageProps>(
371371
const c = configEnv || configContext || imageConfigDefault
372372
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
373373
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
374-
return { ...c, allSizes, deviceSizes }
374+
const qualities = c.qualities?.sort((a, b) => a - b)
375+
return { ...c, allSizes, deviceSizes, qualities }
375376
}, [configContext])
376377

377378
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
@@ -29,7 +29,7 @@ function normalizeSrc(src: string): string {
2929
}
3030

3131
const supportsFloat = typeof ReactDOM.preload === 'function'
32-
32+
const DEFAULT_Q = 75
3333
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
3434
const loadedImageURLs = new Set<string>()
3535
const allImgs = new Map<
@@ -190,8 +190,22 @@ function defaultLoader({
190190
}
191191
}
192192
}
193+
194+
if (quality && config.qualities && !config.qualities.includes(quality)) {
195+
throw new Error(
196+
`Invalid quality prop (${quality}) on \`next/image\` does not match \`images.qualities\` configured in your \`next.config.js\`\n` +
197+
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities`
198+
)
199+
}
193200
}
194201

202+
const q =
203+
quality ||
204+
config.qualities?.reduce((prev, cur) =>
205+
Math.abs(cur - DEFAULT_Q) < Math.abs(prev - DEFAULT_Q) ? cur : prev
206+
) ||
207+
DEFAULT_Q
208+
195209
if (!config.dangerouslyAllowSVG && src.split('?', 1)[0].endsWith('.svg')) {
196210
// Special case to make svg serve as-is to avoid proxying
197211
// through the built-in Image Optimization API.
@@ -200,7 +214,7 @@ function defaultLoader({
200214

201215
return `${normalizePathTrailingSlash(config.path)}?url=${encodeURIComponent(
202216
src
203-
)}&w=${width}&q=${quality || 75}`
217+
)}&w=${width}&q=${q}`
204218
}
205219

206220
const loaders = new Map<
@@ -641,7 +655,8 @@ export default function Image({
641655
const c = configEnv || configContext || imageConfigDefault
642656
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
643657
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
644-
return { ...c, allSizes, deviceSizes }
658+
const qualities = c.qualities?.sort((a, b) => a - b)
659+
return { ...c, allSizes, deviceSizes, qualities }
645660
}, [configContext])
646661

647662
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
@@ -540,6 +540,11 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
540540
loaderFile: z.string().optional(),
541541
minimumCacheTTL: z.number().int().gte(0).optional(),
542542
path: z.string().optional(),
543+
qualities: z
544+
.array(z.number().int().gte(1).lte(100))
545+
.min(1)
546+
.max(20)
547+
.optional(),
543548
})
544549
.optional(),
545550
logging: z

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export class ImageOptimizerCache {
219219
} = imageData
220220
const remotePatterns = nextConfig.images?.remotePatterns || []
221221
const localPatterns = nextConfig.images?.localPatterns
222+
const qualities = nextConfig.images?.qualities
222223
const { url, w, q } = query
223224
let href: string
224225

@@ -334,6 +335,18 @@ export class ImageOptimizerCache {
334335
}
335336
}
336337

338+
if (qualities) {
339+
if (isDev) {
340+
qualities.push(BLUR_QUALITY)
341+
}
342+
343+
if (!qualities.includes(quality)) {
344+
return {
345+
errorMessage: `"q" parameter (quality) of ${q} is not allowed`,
346+
}
347+
}
348+
}
349+
337350
const mimeType = getSupportedMimeType(formats || [], req.headers['accept'])
338351

339352
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
@@ -286,7 +286,8 @@ export function getImgProps(
286286
} else {
287287
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
288288
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
289-
config = { ...c, allSizes, deviceSizes }
289+
const qualities = c.qualities?.sort((a, b) => a - b)
290+
config = { ...c, allSizes, deviceSizes, qualities }
290291
}
291292

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

0 commit comments

Comments
 (0)