Skip to content

Commit

Permalink
Separate config into deviceSizes and iconSizes (#18267)
Browse files Browse the repository at this point in the history
This separates the `next.config.js` property `images.sizes` into to properties: `images.deviceSizes` and `images.iconSizes`.

The purpose is for images that are not intended to take up the majority of the viewport.


Related to #18122
  • Loading branch information
styfle committed Oct 26, 2020
1 parent 557a000 commit 3a169fb
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 34 deletions.
20 changes: 15 additions & 5 deletions docs/basic-features/image-optimization.md
Expand Up @@ -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],
},
}
```
Expand Down Expand Up @@ -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:
Expand All @@ -107,4 +118,3 @@ For more information on what to do next, we recommend the following sections:
<small>See all available properties for the Image component</small>
</a>
</div>

6 changes: 5 additions & 1 deletion packages/next/build/index.ts
Expand Up @@ -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'
)
Expand Down
3 changes: 2 additions & 1 deletion packages/next/build/webpack-config.ts
Expand Up @@ -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
Expand Down
25 changes: 17 additions & 8 deletions packages/next/client/image.tsx
Expand Up @@ -14,7 +14,8 @@ const loaders = new Map<LoaderKey, (props: LoaderProps) => string>([
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default'

type ImageData = {
sizes: number[]
deviceSizes: number[]
iconSizes: number[]
loader: LoaderKey
path: string
domains?: string[]
Expand All @@ -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 =
Expand Down Expand Up @@ -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
Expand All @@ -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 })
}
Expand Down Expand Up @@ -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(', ')
}
Expand Down
44 changes: 36 additions & 8 deletions packages/next/next-server/server/config.ts
Expand Up @@ -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',
Expand Down Expand Up @@ -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(
', '
)})`
)
Expand Down
12 changes: 11 additions & 1 deletion packages/next/next-server/server/image-optimizer.ts
Expand Up @@ -23,14 +23,24 @@ 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,
res: ServerResponse,
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)
Expand Down
3 changes: 2 additions & 1 deletion 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',
},
Expand Down
14 changes: 14 additions & 0 deletions test/integration/image-component/basic/pages/client-side.js
Expand Up @@ -53,6 +53,20 @@ const ClientSide = () => {
width={300}
height={400}
/>
<Image
id="icon-image-64"
src="/icon.png"
loading="eager"
width={64}
height={64}
/>
<Image
id="icon-image-16"
src="/icon.png"
loading="eager"
width={16}
height={16}
/>
<Link href="/errors">
<a id="errorslink">Errors</a>
</Link>
Expand Down
14 changes: 14 additions & 0 deletions test/integration/image-component/basic/pages/index.js
Expand Up @@ -70,6 +70,20 @@ const Page = () => {
width={300}
height={400}
/>
<Image
id="icon-image-64"
src="/icon.png"
loading="eager"
width={64}
height={64}
/>
<Image
id="icon-image-16"
src="/icon.png"
loading="eager"
width={16}
height={16}
/>
<Link href="/client-side">
<a id="clientlink">Client Side</a>
</Link>
Expand Down
14 changes: 14 additions & 0 deletions test/integration/image-component/basic/test/index.test.js
Expand Up @@ -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')
Expand Down

0 comments on commit 3a169fb

Please sign in to comment.