Update default widths configuration to handle 2x/3x DPI (#18717)

- Update default `deviceSizes`
- Add default `imageSizes`
- Use `layout` value to determine which `srcset` to use

Fixes #18420 
Closes #18714
This commit is contained in:
Steven 2020-11-02 21:12:46 -05:00 committed by GitHub
parent 3f84a55ba3
commit 2b94b1eea6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 83 additions and 77 deletions

View file

@ -46,8 +46,8 @@ export default Home
`Image` accepts the following props:
- `src` - The path or URL to the source image. This is required.
- `width` - The intrinsic width of the source image in pixels. Must be an integer without a unit. Required unless `layout="fill"`.
- `height` - The intrinsic height of the source image, in pixels. Must be an integer without a unit. Required unless `layout="fill"`.
- `width` - The width of the image, in pixels. Must be an integer without a unit. Required unless `layout="fill"`.
- `height` - The height of the image, in pixels. Must be an integer without a unit. Required unless `layout="fill"`.
- `layout` - The rendered layout of the image. If `fixed`, the image dimensions will not change as the viewport changes (no responsiveness). If `intrinsic`, the image will scale the dimensions down for smaller viewports but maintain the original dimensions for larger viewports. If `responsive`, the image will scale the dimensions down for smaller viewports and scale up for larger viewports. If `fill`, the image will stretch both width and height to the dimensions of the parent element. Default `intrinsic`.
- `sizes` - Defines what proportion of the screen you expect the image to take up. Recommended, as it helps serve the correct sized image to each device. [More info](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes).
- `quality` - The quality of the optimized image, an integer between 1 and 100 where 100 is the best quality. Default 75.

View file

@ -62,8 +62,8 @@ If no configuration is provided, the following default configuration will be use
```js
module.exports = {
images: {
deviceSizes: [320, 420, 768, 1024, 1200],
imageSizes: [],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
domains: [],
path: '/_next/image',
loader: 'default',
@ -77,24 +77,24 @@ This means you only need to configure the properties you wish to change.
### Device Sizes
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.
You can specify a list of device width breakpoints using the `deviceSizes` property. These widths are used when the [`next/image`](/docs/api-reference/next/image.md) component uses `layout="responsive"` or `layout="fill"` so that the correct image is served for the device visiting your website.
```js
module.exports = {
images: {
deviceSizes: [320, 420, 768, 1024, 1200],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
},
}
```
### Image Sizes
You can specify a list of exact image widths using the `imageSizes` property. These widths should be different than the widths defined in `deviceSizes`. The purpose is for images that don't scale with the browser window, such as icons, badges, or profile images. If the `width` property of a [`next/image`](/docs/api-reference/next/image.md) component matches a value in `imageSizes`, the image will be rendered at that exact width.
You can specify a list of image widths using the `imageSizes` property. These widths should be different than the widths defined in `deviceSizes` because the arrays will be concatentated. These widths are used when the [`next/image`](/docs/api-reference/next/image.md) component uses `layout="fixed"` or `layout="intrinsic"`.
```js
module.exports = {
images: {
imageSizes: [16, 32, 64],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
```
@ -143,7 +143,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 60 seconds is used.
You can configure [`deviceSizes`](#device-sizes) to reduce the total number of possible generated images.
You can configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images.
## Related

View file

@ -12,9 +12,9 @@ Make sure your `images` field follows the allowed config shape and values:
module.exports = {
images: {
// limit of 25 deviceSizes values
deviceSizes: [320, 420, 768, 1024, 1200],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// limit of 25 imageSizes values
imageSizes: [],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// limit of 50 domains values
domains: [],
path: '/_next/image',

View file

@ -63,8 +63,9 @@ const {
domains: configDomains,
} = imageData
// sort smallest to largest
const allSizes = [...configDeviceSizes, ...configImageSizes]
configDeviceSizes.sort((a, b) => a - b)
configImageSizes.sort((a, b) => a - b)
allSizes.sort((a, b) => a - b)
let cachedObserver: IntersectionObserver
@ -105,28 +106,26 @@ function unLazifyImage(lazyImage: HTMLImageElement): void {
lazyImage.classList.remove('__lazy')
}
function getDeviceSizes(
function getSizes(
width: number | undefined,
layout: LayoutValue
): number[] {
): { sizes: number[]; kind: 'w' | 'x' } {
if (
typeof width !== 'number' ||
layout === 'fill' ||
layout === 'responsive'
) {
return configDeviceSizes
return { sizes: configDeviceSizes, kind: 'w' }
}
if (configImageSizes.includes(width)) {
return [width]
}
const widths: number[] = []
for (let size of configDeviceSizes) {
widths.push(size)
if (size >= width) {
break
}
}
return widths
const sizes = [
...new Set(
[width, width * 2, width * 3].map(
(w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1]
)
),
]
return { sizes, kind: 'x' }
}
function computeSrc(
@ -139,8 +138,8 @@ function computeSrc(
if (unoptimized) {
return src
}
const widths = getDeviceSizes(width, layout)
const largest = widths[widths.length - 1]
const { sizes } = getSizes(width, layout)
const largest = sizes[sizes.length - 1]
return callLoader({ src, width: largest, quality })
}
@ -176,8 +175,14 @@ function generateSrcSet({
return undefined
}
return getDeviceSizes(width, layout)
.map((w) => `${callLoader({ src, width: w, quality })} ${w}w`)
const { sizes, kind } = getSizes(width, layout)
return sizes
.map(
(size, i) =>
`${callLoader({ src, width: size, quality })} ${
kind === 'w' ? size : i + 1
}${kind}`
)
.join(', ')
}

View file

@ -25,8 +25,8 @@ const defaultConfig: { [key: string]: any } = {
compress: true,
analyticsId: process.env.VERCEL_ANALYTICS_ID || '',
images: {
deviceSizes: [320, 420, 768, 1024, 1200],
imageSizes: [],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
domains: [],
path: '/_next/image',
loader: 'default',

View file

@ -1,7 +1,7 @@
module.exports = {
images: {
deviceSizes: [480, 1024, 1600, 2000],
imageSizes: [16, 64],
imageSizes: [16, 32, 48, 64],
path: 'https://example.com/myaccount/',
loader: 'imgix',
},

View file

@ -54,11 +54,11 @@ const ClientSide = () => {
height={400}
/>
<Image
id="icon-image-64"
id="icon-image-32"
src="/icon.png"
loading="eager"
width={64}
height={64}
width={32}
height={32}
/>
<Image
id="icon-image-16"

View file

@ -71,11 +71,11 @@ const Page = () => {
height={400}
/>
<Image
id="icon-image-64"
id="icon-image-32"
src="/icon.png"
loading="eager"
width={64}
height={64}
width={32}
height={32}
/>
<Image
id="icon-image-16"

View file

@ -33,46 +33,46 @@ function runTests() {
})
it('should modify src with the loader', async () => {
expect(await browser.elementById('basic-image').getAttribute('src')).toBe(
'https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=480&q=60'
'https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=1024&q=60'
)
})
it('should correctly generate src even if preceding slash is included in prop', async () => {
expect(
await browser.elementById('preceding-slash-image').getAttribute('src')
).toBe(
'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=480'
'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=1024'
)
})
it('should add a srcset based on the loader', async () => {
expect(
await browser.elementById('basic-image').getAttribute('srcset')
).toBe(
'https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=480&q=60 480w'
'https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=480&q=60 1x, https://example.com/myaccount/foo.jpg?auto=format&fit=max&w=1024&q=60 2x'
)
})
it('should add a srcset even with preceding slash in prop', async () => {
expect(
await browser.elementById('preceding-slash-image').getAttribute('srcset')
).toBe(
'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=480 480w'
'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=480 1x, https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=1024 2x'
)
})
it('should use imageSizes 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&fit=max&w=16'
'https://example.com/myaccount/icon.png?auto=format&fit=max&w=48'
)
expect(
await browser.elementById('icon-image-16').getAttribute('srcset')
).toBe(
'https://example.com/myaccount/icon.png?auto=format&fit=max&w=16 16w'
'https://example.com/myaccount/icon.png?auto=format&fit=max&w=16 1x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=32 2x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=48 3x'
)
expect(await browser.elementById('icon-image-64').getAttribute('src')).toBe(
'https://example.com/myaccount/icon.png?auto=format&fit=max&w=64'
expect(await browser.elementById('icon-image-32').getAttribute('src')).toBe(
'https://example.com/myaccount/icon.png?auto=format&fit=max&w=480'
)
expect(
await browser.elementById('icon-image-64').getAttribute('srcset')
await browser.elementById('icon-image-32').getAttribute('srcset')
).toBe(
'https://example.com/myaccount/icon.png?auto=format&fit=max&w=64 64w'
'https://example.com/myaccount/icon.png?auto=format&fit=max&w=32 1x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=64 2x, https://example.com/myaccount/icon.png?auto=format&fit=max&w=480 3x'
)
})
it('should support the unoptimized attribute', async () => {
@ -90,10 +90,10 @@ function runTests() {
function lazyLoadingTests() {
it('should have loaded the first image immediately', async () => {
expect(await browser.elementById('lazy-top').getAttribute('src')).toBe(
'https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=1024'
'https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=2000'
)
expect(await browser.elementById('lazy-top').getAttribute('srcset')).toBe(
'https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=480 480w, https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=1024 1024w'
'https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foo1.jpg?auto=format&fit=max&w=2000 2x'
)
})
it('should not have loaded the second image immediately', async () => {
@ -121,11 +121,11 @@ function lazyLoadingTests() {
await check(() => {
return browser.elementById('lazy-mid').getAttribute('src')
}, 'https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=480')
}, 'https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=1024')
await check(() => {
return browser.elementById('lazy-mid').getAttribute('srcset')
}, 'https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=480 480w')
}, 'https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=480 1x, https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=1024 2x')
})
it('should not have loaded the third image after scrolling down', async () => {
expect(
@ -170,11 +170,11 @@ function lazyLoadingTests() {
await waitFor(200)
expect(
await browser.elementById('lazy-without-attribute').getAttribute('src')
).toBe('https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=1024')
).toBe('https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=2000')
expect(
await browser.elementById('lazy-without-attribute').getAttribute('srcset')
).toBe(
'https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=480 480w, https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=1024 1024w'
'https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=1600 2x, https://example.com/myaccount/foo4.jpg?auto=format&fit=max&w=2000 3x'
)
})
@ -220,14 +220,14 @@ describe('Image Component Tests', () => {
it('should add a preload tag for a priority image', async () => {
expect(
await hasPreloadLinkMatchingUrl(
'https://example.com/myaccount/withpriority.png?auto=format&fit=max&w=480&q=60'
'https://example.com/myaccount/withpriority.png?auto=format&fit=max&w=1024&q=60'
)
).toBe(true)
})
it('should add a preload tag for a priority image with preceding slash', async () => {
expect(
await hasPreloadLinkMatchingUrl(
'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=480'
'https://example.com/myaccount/fooslash.jpg?auto=format&fit=max&w=1024'
)
).toBe(true)
})
@ -241,7 +241,7 @@ describe('Image Component Tests', () => {
it('should add a preload tag for a priority image, with quality', async () => {
expect(
await hasPreloadLinkMatchingUrl(
'https://example.com/myaccount/withpriority.png?auto=format&fit=max&w=480&q=60'
'https://example.com/myaccount/withpriority.png?auto=format&fit=max&w=1024&q=60'
)
).toBe(true)
})
@ -295,12 +295,12 @@ describe('Image Component Tests', () => {
expect(
await browser.elementById('lazy-no-observer').getAttribute('src')
).toBe(
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024'
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000'
)
expect(
await browser.elementById('lazy-no-observer').getAttribute('srcset')
).toBe(
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=480 480w, https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024 1024w'
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000 2x'
)
})
})
@ -321,12 +321,12 @@ describe('Image Component Tests', () => {
expect(
await browser.elementById('lazy-no-observer').getAttribute('src')
).toBe(
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024'
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000'
)
expect(
await browser.elementById('lazy-no-observer').getAttribute('srcset')
).toBe(
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=480 480w, https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024 1024w'
'https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=1024 1x, https://example.com/myaccount/foox.jpg?auto=format&fit=max&w=2000 2x'
)
})
})

View file

@ -80,7 +80,7 @@ function runTests(mode) {
expect(
await hasImageMatchingUrl(
browser,
`http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=420&q=75`
`http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=1200&q=75`
)
).toBe(true)
} finally {
@ -120,10 +120,10 @@ function runTests(mode) {
const delta = 250
const id = 'fixed1'
expect(await getSrc(browser, id)).toBe(
'/_next/image?url=%2Fwide.png&w=1200&q=75'
'/_next/image?url=%2Fwide.png&w=3840&q=75'
)
expect(await browser.elementById(id).getAttribute('srcset')).toBe(
'/_next/image?url=%2Fwide.png&w=320&q=75 320w, /_next/image?url=%2Fwide.png&w=420&q=75 420w, /_next/image?url=%2Fwide.png&w=768&q=75 768w, /_next/image?url=%2Fwide.png&w=1024&q=75 1024w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w'
'/_next/image?url=%2Fwide.png&w=1200&q=75 1x, /_next/image?url=%2Fwide.png&w=3840&q=75 2x'
)
await browser.setDimensions({
width: width + delta,
@ -153,10 +153,10 @@ function runTests(mode) {
const delta = 250
const id = 'intrinsic1'
expect(await getSrc(browser, id)).toBe(
'/_next/image?url=%2Fwide.png&w=1200&q=75'
'/_next/image?url=%2Fwide.png&w=3840&q=75'
)
expect(await browser.elementById(id).getAttribute('srcset')).toBe(
'/_next/image?url=%2Fwide.png&w=320&q=75 320w, /_next/image?url=%2Fwide.png&w=420&q=75 420w, /_next/image?url=%2Fwide.png&w=768&q=75 768w, /_next/image?url=%2Fwide.png&w=1024&q=75 1024w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w'
'/_next/image?url=%2Fwide.png&w=1200&q=75 1x, /_next/image?url=%2Fwide.png&w=3840&q=75 2x'
)
await browser.setDimensions({
width: width + delta,
@ -189,10 +189,10 @@ function runTests(mode) {
const delta = 250
const id = 'responsive1'
expect(await getSrc(browser, id)).toBe(
'/_next/image?url=%2Fwide.png&w=1200&q=75'
'/_next/image?url=%2Fwide.png&w=3840&q=75'
)
expect(await browser.elementById(id).getAttribute('srcset')).toBe(
'/_next/image?url=%2Fwide.png&w=320&q=75 320w, /_next/image?url=%2Fwide.png&w=420&q=75 420w, /_next/image?url=%2Fwide.png&w=768&q=75 768w, /_next/image?url=%2Fwide.png&w=1024&q=75 1024w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w'
'/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w'
)
await browser.setDimensions({
width: width + delta,
@ -225,10 +225,10 @@ function runTests(mode) {
const delta = 150
const id = 'fill1'
expect(await getSrc(browser, id)).toBe(
'/_next/image?url=%2Fwide.png&w=1200&q=75'
'/_next/image?url=%2Fwide.png&w=3840&q=75'
)
expect(await browser.elementById(id).getAttribute('srcset')).toBe(
'/_next/image?url=%2Fwide.png&w=320&q=75 320w, /_next/image?url=%2Fwide.png&w=420&q=75 420w, /_next/image?url=%2Fwide.png&w=768&q=75 768w, /_next/image?url=%2Fwide.png&w=1024&q=75 1024w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w'
'/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w'
)
await browser.setDimensions({
width: width + delta,
@ -261,10 +261,10 @@ function runTests(mode) {
const height = await getComputed(browser, id, 'height')
await browser.eval(`document.getElementById("${id}").scrollIntoView()`)
expect(await getSrc(browser, id)).toBe(
'/_next/image?url=%2Fwide.png&w=1200&q=75'
'/_next/image?url=%2Fwide.png&w=3840&q=75'
)
expect(await browser.elementById(id).getAttribute('srcset')).toBe(
'/_next/image?url=%2Fwide.png&w=320&q=75 320w, /_next/image?url=%2Fwide.png&w=420&q=75 420w, /_next/image?url=%2Fwide.png&w=768&q=75 768w, /_next/image?url=%2Fwide.png&w=1024&q=75 1024w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w'
'/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w'
)
expect(await getComputed(browser, id, 'width')).toBe(width)
expect(await getComputed(browser, id, 'height')).toBe(height)

View file

@ -19,7 +19,7 @@ jest.setTimeout(1000 * 60 * 2)
const appDir = join(__dirname, '../')
const imagesDir = join(appDir, '.next', 'cache', 'images')
const nextConfig = new File(join(appDir, 'next.config.js'))
const largeSize = 1024
const largeSize = 1080 // defaults defined in server/config.ts
let appPort
let app
@ -441,7 +441,7 @@ describe('Image Optimizer', () => {
const domains = ['localhost', 'example.com']
describe('dev support w/o next.config.js', () => {
const size = 320 // defaults defined in server/config.ts
const size = 384 // defaults defined in server/config.ts
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
@ -478,7 +478,7 @@ describe('Image Optimizer', () => {
})
describe('Server support w/o next.config.js', () => {
const size = 320 // defaults defined in server/config.ts
const size = 384 // defaults defined in server/config.ts
beforeAll(async () => {
await nextBuild(appDir)
appPort = await findPort()
@ -557,7 +557,8 @@ describe('Image Optimizer', () => {
await fs.remove(imagesDir)
})
it('should 404 when loader is not default', async () => {
const query = { w: 320, q: 90, url: '/test.svg' }
const size = 384 // defaults defined in server/config.ts
const query = { w: size, q: 90, url: '/test.svg' }
const opts = { headers: { accept: 'image/webp' } }
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(404)