diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx
index a38b40e1cafa099..457b2211d1869ef 100644
--- a/packages/next/client/image.tsx
+++ b/packages/next/client/image.tsx
@@ -42,7 +42,6 @@ const {
domains: configDomains,
} = imageData
configSizes.sort((a, b) => a - b) // smallest to largest
-const largestSize = configSizes[configSizes.length - 1]
let cachedObserver: IntersectionObserver
const IntersectionObserver =
@@ -80,15 +79,32 @@ function getObserver(): IntersectionObserver | undefined {
))
}
+function getWidthsFromConfig(width: number | undefined) {
+ if (typeof width !== 'number') {
+ return configSizes
+ }
+ const widths: number[] = []
+ for (let size of configSizes) {
+ widths.push(size)
+ if (size >= width) {
+ break
+ }
+ }
+ return widths
+}
+
function computeSrc(
src: string,
unoptimized: boolean,
+ width: number | undefined,
quality?: string
): string {
if (unoptimized) {
return src
}
- return callLoader({ src, width: largestSize, quality })
+ const widths = getWidthsFromConfig(width)
+ const largest = widths[widths.length - 1]
+ return callLoader({ src, width: largest, quality })
}
type CallLoaderProps = {
@@ -104,29 +120,38 @@ function callLoader(loaderProps: CallLoaderProps) {
type SrcSetData = {
src: string
- widths: number[]
- quality?: string
+ unoptimized: boolean
+ width: number | undefined
+ quality: string | undefined
}
-function generateSrcSet({ src, widths, quality }: SrcSetData): string {
+function generateSrcSet({
+ src,
+ unoptimized,
+ width,
+ quality,
+}: SrcSetData): string | undefined {
// At each breakpoint, generate an image url using the loader, such as:
// ' www.example.com/foo.jpg?w=480 480w, '
- return widths
- .map((width: number) => `${callLoader({ src, width, quality })} ${width}w`)
+ if (unoptimized) {
+ return undefined
+ }
+ return getWidthsFromConfig(width)
+ .map((w) => `${callLoader({ src, width: w, quality })} ${w}w`)
.join(', ')
}
type PreloadData = {
src: string
- widths: number[]
+ unoptimized: boolean
+ width: number | undefined
sizes?: string
- unoptimized?: boolean
quality?: string
}
function generatePreload({
src,
- widths,
+ width,
unoptimized = false,
sizes,
quality,
@@ -140,15 +165,25 @@ function generatePreload({
)
}
+function getInt(x: unknown): number | undefined {
+ if (typeof x === 'number') {
+ return x
+ }
+ if (typeof x === 'string') {
+ return parseInt(x, 10)
+ }
+ return undefined
+}
+
export default function Image({
src,
sizes,
@@ -165,6 +200,13 @@ export default function Image({
const thisEl = useRef(null)
if (process.env.NODE_ENV !== 'production') {
+ if (!src) {
+ throw new Error(
+ `Image is missing required "src" property. Make sure you pass "src" in props to the \`next/image\` component. Received: ${JSON.stringify(
+ { width, height, quality, unsized }
+ )}`
+ )
+ }
if (!VALID_LOADING_VALUES.includes(loading)) {
throw new Error(
`Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map(
@@ -200,58 +242,23 @@ export default function Image({
}
}, [thisEl, lazy])
- // Generate attribute values
- const imgSrc = computeSrc(src, unoptimized, quality)
- const imgSrcSet = !unoptimized
- ? generateSrcSet({
- src,
- widths: configSizes,
- quality,
- })
- : undefined
-
- let imgAttributes:
- | {
- src: string
- srcSet?: string
- }
- | {
- 'data-src': string
- 'data-srcset'?: string
- }
- if (!lazy) {
- imgAttributes = {
- src: imgSrc,
- }
- if (imgSrcSet) {
- imgAttributes.srcSet = imgSrcSet
- }
- } else {
- imgAttributes = {
- 'data-src': imgSrc,
- }
- if (imgSrcSet) {
- imgAttributes['data-srcset'] = imgSrcSet
- }
- className = className ? className + ' __lazy' : '__lazy'
- }
-
+ let widthInt = getInt(width)
+ let heightInt = getInt(height)
let divStyle: React.CSSProperties | undefined
let imgStyle: React.CSSProperties | undefined
let wrapperStyle: React.CSSProperties | undefined
if (
- typeof height !== 'undefined' &&
- typeof width !== 'undefined' &&
+ typeof widthInt !== 'undefined' &&
+ typeof heightInt !== 'undefined' &&
!unsized
) {
//
//
- const quotient =
- parseInt(height as string, 10) / parseInt(width as string, 10)
+ const quotient = heightInt / widthInt
const ratio = isNaN(quotient) ? 1 : quotient * 100
wrapperStyle = {
maxWidth: '100%',
- width,
+ width: widthInt,
}
divStyle = {
position: 'relative',
@@ -266,8 +273,8 @@ export default function Image({
width: '100%',
}
} else if (
- typeof height === 'undefined' &&
- typeof width === 'undefined' &&
+ typeof widthInt === 'undefined' &&
+ typeof heightInt === 'undefined' &&
unsized
) {
//
@@ -288,6 +295,41 @@ export default function Image({
}
}
+ // Generate attribute values
+ const imgSrc = computeSrc(src, unoptimized, widthInt, quality)
+ const imgSrcSet = generateSrcSet({
+ src,
+ width: widthInt,
+ unoptimized,
+ quality,
+ })
+
+ let imgAttributes:
+ | {
+ src: string
+ srcSet?: string
+ }
+ | {
+ 'data-src': string
+ 'data-srcset'?: string
+ }
+ if (!lazy) {
+ imgAttributes = {
+ src: imgSrc,
+ }
+ if (imgSrcSet) {
+ imgAttributes.srcSet = imgSrcSet
+ }
+ } else {
+ imgAttributes = {
+ 'data-src': imgSrc,
+ }
+ if (imgSrcSet) {
+ imgAttributes['data-srcset'] = imgSrcSet
+ }
+ className = className ? className + ' __lazy' : '__lazy'
+ }
+
// No need to add preloads on the client side--by the time the application is hydrated,
// it's too late for preloads
const shouldPreload = priority && typeof window === 'undefined'
@@ -298,7 +340,7 @@ export default function Image({
{shouldPreload
? generatePreload({
src,
- widths: configSizes,
+ width: widthInt,
unoptimized,
sizes,
quality,
diff --git a/test/integration/image-component/basic/next.config.js b/test/integration/image-component/basic/next.config.js
index 8f260dea043fdba..6adf797d1c2b1e5 100644
--- a/test/integration/image-component/basic/next.config.js
+++ b/test/integration/image-component/basic/next.config.js
@@ -1,6 +1,6 @@
module.exports = {
images: {
- sizes: [480, 1024, 1600],
+ sizes: [480, 1024, 1600, 2000],
path: 'https://example.com/myaccount/',
loader: 'imgix',
},
diff --git a/test/integration/image-component/basic/pages/index.js b/test/integration/image-component/basic/pages/index.js
index 0ad8d8e1ac445dd..a930e0ef1ef8019 100644
--- a/test/integration/image-component/basic/pages/index.js
+++ b/test/integration/image-component/basic/pages/index.js
@@ -19,7 +19,7 @@ const Page = () => {
data-demo="demo-value"
src="bar.jpg"
loading="eager"
- width={300}
+ width={1024}
height={400}
/>
{
width={300}
height={400}
/>
-
{
id="lazy-top"
src="foo1.jpg"
height={400}
- width={300}
+ width={1024}
loading="lazy"
>
@@ -35,7 +35,7 @@ const Lazy = () => {
id="lazy-without-attribute"
src="foo4.jpg"
height={400}
- width={300}
+ width={800}
>
{
src="foo5.jpg"
loading="eager"
height={400}
- width={300}
+ width={1900}
>
)
diff --git a/test/integration/image-component/basic/test/index.test.js b/test/integration/image-component/basic/test/index.test.js
index bda3f67eea792e2..9e9198cdac42b0f 100644
--- a/test/integration/image-component/basic/test/index.test.js
+++ b/test/integration/image-component/basic/test/index.test.js
@@ -33,27 +33,23 @@ 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&w=1600&q=60'
+ 'https://example.com/myaccount/foo.jpg?auto=format&w=480&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&w=1600')
+ ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480')
})
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&w=480&q=60 480w, https://example.com/myaccount/foo.jpg?auto=format&w=1024&q=60 1024w, https://example.com/myaccount/foo.jpg?auto=format&w=1600&q=60 1600w'
- )
+ ).toBe('https://example.com/myaccount/foo.jpg?auto=format&w=480&q=60 480w')
})
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&w=480 480w, https://example.com/myaccount/fooslash.jpg?auto=format&w=1024 1024w, https://example.com/myaccount/fooslash.jpg?auto=format&w=1600 1600w'
- )
+ ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480 480w')
})
it('should support the unoptimized attribute', async () => {
expect(
@@ -70,10 +66,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&w=1600'
+ 'https://example.com/myaccount/foo1.jpg?auto=format&w=1024'
)
expect(await browser.elementById('lazy-top').getAttribute('srcset')).toBe(
- 'https://example.com/myaccount/foo1.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo1.jpg?auto=format&w=1024 1024w, https://example.com/myaccount/foo1.jpg?auto=format&w=1600 1600w'
+ 'https://example.com/myaccount/foo1.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo1.jpg?auto=format&w=1024 1024w'
)
})
it('should not have loaded the second image immediately', async () => {
@@ -101,11 +97,11 @@ function lazyLoadingTests() {
await check(() => {
return browser.elementById('lazy-mid').getAttribute('src')
- }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=1600')
+ }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480')
await check(() => {
return browser.elementById('lazy-mid').getAttribute('srcset')
- }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo2.jpg?auto=format&w=1024 1024w, https://example.com/myaccount/foo2.jpg?auto=format&w=1600 1600w')
+ }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480 480w')
})
it('should not have loaded the third image after scrolling down', async () => {
expect(
@@ -150,15 +146,17 @@ function lazyLoadingTests() {
await waitFor(200)
expect(
await browser.elementById('lazy-without-attribute').getAttribute('src')
- ).toBe('https://example.com/myaccount/foo4.jpg?auto=format&w=1600')
+ ).toBe('https://example.com/myaccount/foo4.jpg?auto=format&w=1024')
expect(
await browser.elementById('lazy-without-attribute').getAttribute('srcset')
- ).toBeTruthy()
+ ).toBe(
+ 'https://example.com/myaccount/foo4.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo4.jpg?auto=format&w=1024 1024w'
+ )
})
it('should load the fifth image eagerly, without scrolling', async () => {
expect(await browser.elementById('eager-loading').getAttribute('src')).toBe(
- 'https://example.com/myaccount/foo5.jpg?auto=format&w=1600'
+ 'https://example.com/myaccount/foo5.jpg?auto=format&w=2000'
)
expect(
await browser.elementById('eager-loading').getAttribute('srcset')
@@ -198,14 +196,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&w=1600'
+ 'https://example.com/myaccount/withpriority.png?auto=format&w=480&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&w=1600'
+ 'https://example.com/myaccount/fooslash.jpg?auto=format&w=480'
)
).toBe(true)
})
@@ -219,7 +217,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&w=1600&q=60'
+ 'https://example.com/myaccount/withpriority.png?auto=format&w=480&q=60'
)
).toBe(true)
})
diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js
index 8af093b9240c4ec..c2c62f141278025 100644
--- a/test/integration/image-component/default/test/index.test.js
+++ b/test/integration/image-component/default/test/index.test.js
@@ -88,7 +88,7 @@ function runTests(mode) {
await hasRedbox(browser)
expect(await getRedboxHeader(browser)).toContain(
- 'Next Image Optimization requires src to be provided. Make sure you pass them as props to the `next/image` component. Received: {"width":1200}'
+ 'Image is missing required "src" property. Make sure you pass "src" in props to the `next/image` component. Received: {"width":200}'
)
})