Skip to content

Commit

Permalink
Add support for AVIF to next/image (#29683)
Browse files Browse the repository at this point in the history
Add support for AVIF to `next/image`

- Fixes #27882 
- Closes #27432 

## Feature

- [x] Implements an existing feature request
- [x] Related issues linked
- [x] Integration tests added
- [x] Documentation added
- [x] Update manifest output
- [x] Warn when `sharp` is outdated
- [x] Errors & Warnings have helpful link attached
- [ ] Remove `image-size` in favor of `squoosh`/`sharp` (optional, need to benchmark)
  • Loading branch information
styfle authored Oct 11, 2021
1 parent 13f68de commit cc1f3b8
Show file tree
Hide file tree
Showing 30 changed files with 302 additions and 43 deletions.
20 changes: 19 additions & 1 deletion docs/api-reference/next/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component.

| Version | Changes |
| --------- | ------------------------------------------------------------------------------------------------- |
| `v12.0.0` | `formats` configuration added as well as AVIF support. |
| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. |
| `v11.0.0` | `src` prop support for static import.<br/>`placeholder` prop added.<br/>`blurDataURL` prop added. |
| `v10.0.5` | `loader` prop added. |
Expand Down Expand Up @@ -141,7 +142,7 @@ Should only be used when the image is visible above the fold. Defaults to `false

A placeholder to use while the image is loading. Possible values are `blur` or `empty`. Defaults to `empty`.

When `blur`, the [`blurDataURL`](#blurdataurl) property will be used as the placeholder. If `src` is an object from a [static import](#local-images) and the imported image is `.jpg`, `.png`, or `.webp`, then `blurDataURL` will be automatically populated.
When `blur`, the [`blurDataURL`](#blurdataurl) property will be used as the placeholder. If `src` is an object from a [static import](#local-images) and the imported image is `.jpg`, `.png`, `.webp`, or `.avif`, then `blurDataURL` will be automatically populated.

For dynamic images, you must provide the [`blurDataURL`](#blurdataurl) property. Solutions such as [Plaiceholder](https://github.com/joe-bell/plaiceholder) can help with `base64` generation.

Expand Down Expand Up @@ -322,6 +323,7 @@ The expiration (or rather Max Age) is defined by the upstream server's `Cache-Co
- If `s-maxage` is found in `Cache-Control`, it is used. If no `s-maxage` is found, then `max-age` is used. If no `max-age` is found, then [`minimumCacheTTL`](#minimum-cache-ttl) is used.
- You can configure [`minimumCacheTTL`](#minimum-cache-ttl) to increase the cache duration when the upstream image does not include `max-age`.
- You can also configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images.
- You can also configure [formats](/docs/basic-features/image-optimization.md#acceptable-formats) to disable multiple formats in favor of a single image format.

You can configure the Time to Live (TTL) in seconds for cached optimized images. In many cases, it's better to use a [Static Image Import](/docs/basic-features/image-optimization.md#local-images) which will automatically hash the file contents and cache the image forever with a `Cache-Control` header of `immutable`.

Expand Down Expand Up @@ -351,6 +353,22 @@ module.exports = {
}
```

### Acceptable Formats

The default [Image Optimization API](#loader-configuration) will automatically detect the browser's supported image formats via the request's `Accept` header.

If the `Accept` matches more than one of the configured formats, the first match in the array is used. Therefore, the array order matters. If there is no match, the Image Optimization API will fallback to the original image's format.

If no configuration is provided, the default below is used.

```js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
},
}
```

## Related

For an overview of the Image component features and usage guidelines, see:
Expand Down
2 changes: 1 addition & 1 deletion docs/basic-features/image-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ To use a local image, `import` your `.jpg`, `.png`, or `.webp` files:
import profilePic from '../public/me.png'
```

Dynamic `await import()` or `require()` are _not_ supported. The `import` must be static. Also note that static image support requires Webpack 5, which is enabled by default in Next.js applications.
Dynamic `await import()` or `require()` are _not_ supported. The `import` must be static so it can be analyzed at build time.

Next.js will automatically determine the `width` and `height` of your image based on the imported file. These values are used to prevent [Cumulative Layout Shift](https://nextjs.org/learn/seo/web-performance/cls) while your image is loading.

Expand Down
3 changes: 2 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ module.exports = {

/* Handle image imports
https://jestjs.io/docs/webpack#handling-static-assets */
'^.+\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js',
'^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$':
'<rootDir>/__mocks__/fileMock.js',
},
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
testEnvironment: 'jsdom',
Expand Down
4 changes: 4 additions & 0 deletions errors/invalid-images-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@ module.exports = {
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// limit of 50 domains values
domains: [],
// path prefix for Image Optimization API, useful with `loader`
path: '/_next/image',
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom'
loader: 'default',
// disable static imports for image files
disableStaticImages: false,
// minimumCacheTTL is in seconds, must be integer 0 or more
minimumCacheTTL: 60,
// ordered list of acceptable optimized image formats (mime types)
formats: ['image/avif', 'image/webp'],
},
}
```

### Useful Links

- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)
4 changes: 4 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@
"title": "sharp-missing-in-production",
"path": "/errors/sharp-missing-in-production.md"
},
{
"title": "sharp-version-avif",
"path": "/errors/sharp-version-avif.md"
},
{
"title": "script-in-document-page",
"path": "/errors/no-script-in-document-page.md"
Expand Down
2 changes: 1 addition & 1 deletion errors/placeholder-blur-data-url.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ You are attempting use the `next/image` component with `placeholder=blur` proper

The `blurDataURL` might be missing because you're using a string for `src` instead of a static import.

Or `blurDataURL` might be missing because the static import is an unsupported image format. Only jpg, png, and webp are supported at this time.
Or `blurDataURL` might be missing because the static import is an unsupported image format. Only jpg, png, webp, and avif are supported at this time.

#### Possible Ways to Fix It

Expand Down
1 change: 1 addition & 0 deletions errors/sharp-missing-in-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ You are seeing this error because Image Optimization in production mode (`next s
### Useful Links

- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)
24 changes: 24 additions & 0 deletions errors/sharp-version-avif.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Sharp Version Does Not Support AVIF

#### Why This Error Occurred

The `next/image` component's default loader uses [`sharp`](https://www.npmjs.com/package/sharp) if its installed.

You are seeing this error because you have an outdated version of [`sharp`](https://www.npmjs.com/package/sharp) installed that does not support the AVIF image format.

AVIF support was added to [`sharp`](https://www.npmjs.com/package/sharp) in version 0.27.0 (December 2020) so your installed version is likely older.

#### Possible Ways to Fix It

- Install the latest version of `sharp` by running `yarn add sharp@latest` in your project directory
- If you're using the `NEXT_SHARP_PATH` environment variable, then update the `sharp` install referenced in that path, for example `cd "$NEXT_SHARP_PATH/../" && yarn add sharp@latest`
- If you cannot upgrade `sharp`, you can instead disable AVIF by configuring [`formats`](https://nextjs.org/docs/api-reference/next/image#image-formats) in your `next.config.js`

After choosing an option above, reboot the server by running either `next dev` or `next start` for development or production respectively.

> Note: This is not necessary for Vercel deployments, since `sharp` is installed automatically for you.
### Useful Links

- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization)
- [`next/image` Documentation](https://nextjs.org/docs/api-reference/next/image)
2 changes: 1 addition & 1 deletion examples/with-jest/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {

// Handle image imports
// https://jestjs.io/docs/webpack#handling-static-assets
'^.+\\.(jpg|jpeg|png|gif|webp|svg)$': `<rootDir>/__mocks__/fileMock.js`,
'^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$': `<rootDir>/__mocks__/fileMock.js`,

// Handle module aliases
'^@/components/(.*)$': '<rootDir>/components/$1',
Expand Down
4 changes: 2 additions & 2 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ export default async function getBaseWebpackConfig(
...(!config.images.disableStaticImages
? [
{
test: /\.(png|jpg|jpeg|gif|webp|ico|bmp|svg)$/i,
test: /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i,
loader: 'next-image-loader',
issuer: { not: regexLikeCss },
dependency: { not: ['url'] },
Expand Down Expand Up @@ -1512,7 +1512,7 @@ export default async function getBaseWebpackConfig(
// Exclude svg if the user already defined it in custom
// webpack config such as `@svgr/webpack` plugin or
// the `babel-plugin-inline-react-svg` plugin.
nextImageRule.test = /\.(png|jpg|jpeg|gif|webp|ico|bmp)$/i
nextImageRule.test = /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp)$/i
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/webpack/config/blocks/images/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const images = curry(async function images(
loader({
oneOf: [
{
test: /\.(png|jpg|jpeg|gif|webp|ico|bmp|svg)$/i,
test: /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i,
use: {
loader: 'error-loader',
options: {
Expand Down
9 changes: 5 additions & 4 deletions packages/next/build/webpack/loaders/next-image-loader.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import loaderUtils from 'next/dist/compiled/loader-utils'
import sizeOf from 'image-size'
import { resizeImage } from '../../../server/image-optimizer'
import { resizeImage, getImageSize } from '../../../server/image-optimizer'

const BLUR_IMG_SIZE = 8
const BLUR_QUALITY = 70
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp']
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next/client/image.tsx

function nextImageLoader(content) {
const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader')
Expand All @@ -26,7 +25,9 @@ function nextImageLoader(content) {
}

const imageSizeSpan = imageLoaderSpan.traceChild('image-size-calculation')
const imageSize = imageSizeSpan.traceFn(() => sizeOf(content))
const imageSize = await imageSizeSpan.traceAsyncFn(() =>
getImageSize(content, extension)
)
let blurDataURL

if (VALID_BLUR_EXT.includes(extension)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export default function Image({
)
}
if (!blurDataURL) {
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp'] // should match next-image-loader
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader

throw new Error(
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.
Expand Down
6 changes: 6 additions & 0 deletions packages/next/image-types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ declare module '*.webp' {
export default content
}

declare module '*.avif' {
const content: StaticImageData

export default content
}

declare module '*.ico' {
const content: StaticImageData

Expand Down
26 changes: 26 additions & 0 deletions packages/next/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,32 @@ function assignDefaults(userConfig: { [key: string]: any }) {
)}), received (${images.minimumCacheTTL}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}

if (images.formats) {
const { formats } = images
if (!Array.isArray(formats)) {
throw new Error(
`Specified images.formats should be an Array received ${typeof formats}.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
if (formats.length < 1 || formats.length > 2) {
throw new Error(
`Specified images.formats must be length 1 or 2, received length (${formats.length}), please reduce the length of the array to continue.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}

const invalid = formats.filter((f) => {
return f !== 'image/avif' && f !== 'image/webp'
})

if (invalid.length > 0) {
throw new Error(
`Specified images.formats should be an Array of mime type strings, received invalid values (${invalid.join(
', '
)}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
}
}

if (result.webpack5 === false) {
Expand Down
4 changes: 4 additions & 0 deletions packages/next/server/image-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const VALID_LOADERS = [

export type LoaderValue = typeof VALID_LOADERS[number]

type ImageFormat = 'image/avif' | 'image/webp'

export type ImageConfigComplete = {
deviceSizes: number[]
imageSizes: number[]
Expand All @@ -16,6 +18,7 @@ export type ImageConfigComplete = {
domains?: string[]
disableStaticImages?: boolean
minimumCacheTTL?: number
formats?: ImageFormat[]
}

export type ImageConfig = Partial<ImageConfigComplete>
Expand All @@ -28,4 +31,5 @@ export const imageConfigDefault: ImageConfigComplete = {
domains: [],
disableStaticImages: false,
minimumCacheTTL: 60,
formats: ['image/avif', 'image/webp'],
}
Loading

0 comments on commit cc1f3b8

Please sign in to comment.