mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
feat(next/image): introduce preload prop and deprecated priority prop (#83351)
The Image component's existing `priority` prop is confusing since its not obvious what it does. In order to improve clarity, we are introducing a `preload` prop and deprecating the `priority` prop. We also updated the dev warning to no longer suggest using this prop for the LCP and instead use `loading="eager"`.
This commit is contained in:
@@ -45,7 +45,7 @@ The following props are available:
|
||||
| [`loader`](#loader) | `loader={imageLoader}` | Function | - |
|
||||
| [`sizes`](#sizes) | `sizes="(max-width: 768px) 100vw, 33vw"` | String | - |
|
||||
| [`quality`](#quality) | `quality={80}` | Integer (1-100) | - |
|
||||
| [`priority`](#priority) | `priority={true}` | Boolean | - |
|
||||
| [`preload`](#preload) | `preload={true}` | Boolean | - |
|
||||
| [`placeholder`](#placeholder) | `placeholder="blur"` | String | - |
|
||||
| [`style`](#style) | `style={{objectFit: "contain"}}` | Object | - |
|
||||
| [`onLoadingComplete`](#onloadingcomplete) | `onLoadingComplete={img => done())}` | Function | Deprecated |
|
||||
@@ -259,27 +259,35 @@ export default function ProfileImage() {
|
||||
|
||||
> **Good to know**: If you’re using the `style` prop to set a custom width, be sure to also set `height: 'auto'` to preserve the image’s aspect ratio.
|
||||
|
||||
#### `priority`
|
||||
#### `preload`
|
||||
|
||||
A boolean that indicates if the image should be preloaded.
|
||||
|
||||
```jsx
|
||||
// Default priority is false
|
||||
<Image priority={false} />
|
||||
// Default preload is false
|
||||
<Image preload={false} />
|
||||
```
|
||||
|
||||
- `true`: [Preloads](https://web.dev/preload-responsive-images/) the image. Disables lazy loading.
|
||||
- `false`: Lazy loads the image.
|
||||
- `true`: [Preloads](https://web.dev/preload-responsive-images/) the image by inserting a `<link>` in the `<head>`.
|
||||
- `false`: Does not preload the image.
|
||||
|
||||
**When to use it:**
|
||||
|
||||
- The image is above the fold.
|
||||
- The image is the [Largest Contentful Paint (LCP)](https://nextjs.org/learn/seo/web-performance/lcp) element.
|
||||
- You want to improve the initial loading performance of your page.
|
||||
- The image is above the fold, typically the hero image.
|
||||
- You want to begin loading the image in the `<head>`, before its discovered later in the `<body>`.
|
||||
|
||||
**When not to use it:**
|
||||
|
||||
- When the `loading` prop is used (will trigger warnings).
|
||||
- When you have multiple images that could be considered the [Largest Contentful Paint (LCP)](https://nextjs.org/learn/seo/web-performance/lcp) element depending on the viewport.
|
||||
- When the `loading` property is used.
|
||||
- When the `fetchPriority` property is used.
|
||||
|
||||
In most cases, you should use `loading="eager"` or `fetchPriority="high"` instead of `preload`.
|
||||
|
||||
#### `priority`
|
||||
|
||||
Starting with Next.js 16, the `priority` property has been deprecated in favor of the [`preload`](#preload) property in order to make the behavior clear.
|
||||
|
||||
#### `loading`
|
||||
|
||||
@@ -908,7 +916,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c
|
||||
|
||||
- [Safari 15 - 16.3](https://bugs.webkit.org/show_bug.cgi?id=243601) display a gray border while loading. Safari 16.4 [fixed this issue](https://webkit.org/blog/13966/webkit-features-in-safari-16-4/#:~:text=Now%20in%20Safari%2016.4%2C%20a%20gray%20line%20no%20longer%20appears%20to%20mark%20the%20space%20where%20a%20lazy%2Dloaded%20image%20will%20appear%20once%20it%E2%80%99s%20been%20loaded.). Possible solutions:
|
||||
- Use CSS `@supports (font: -apple-system-body) and (-webkit-appearance: none) { img[loading="lazy"] { clip-path: inset(0.6px) } }`
|
||||
- Use [`priority`](#priority) if the image is above the fold
|
||||
- Use [`loading="eager"`](#loading) if the image is above the fold
|
||||
- [Firefox 67+](https://bugzilla.mozilla.org/show_bug.cgi?id=1556156) displays a white background while loading. Possible solutions:
|
||||
- Enable [AVIF `formats`](#formats)
|
||||
- Use [`placeholder`](#placeholder)
|
||||
@@ -1155,7 +1163,7 @@ If you want to display a different image for light and dark mode, you can create
|
||||
import styles from './theme-image.module.css'
|
||||
import Image, { ImageProps } from 'next/image'
|
||||
|
||||
type Props = Omit<ImageProps, 'src' | 'priority' | 'loading'> & {
|
||||
type Props = Omit<ImageProps, 'src' | 'preload' | 'loading'> & {
|
||||
srcLight: string
|
||||
srcDark: string
|
||||
}
|
||||
@@ -1188,7 +1196,7 @@ const ThemeImage = (props) => {
|
||||
}
|
||||
```
|
||||
|
||||
> **Good to know**: The default behavior of `loading="lazy"` ensures that only the correct image is loaded. You cannot use `priority` or `loading="eager"` because that would cause both images to load. Instead, you can use [`fetchPriority="high"`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement/fetchPriority).
|
||||
> **Good to know**: The default behavior of `loading="lazy"` ensures that only the correct image is loaded. You cannot use `preload` or `loading="eager"` because that would cause both images to load. Instead, you can use [`fetchPriority="high"`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement/fetchPriority).
|
||||
|
||||
Try it out:
|
||||
|
||||
@@ -1269,7 +1277,7 @@ export default function Home() {
|
||||
|
||||
| Version | Changes |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `v16.0.0` | `qualities` default configuration changed to `[75]`. |
|
||||
| `v16.0.0` | `qualities` default configuration changed to `[75]`, `preload` prop added, `priority` prop deprecated. |
|
||||
| `v15.3.0` | `remotePatterns` added support for array of `URL` objects. |
|
||||
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |
|
||||
| `v14.2.23` | `qualities` configuration added. |
|
||||
|
||||
@@ -799,5 +799,7 @@
|
||||
"798": "expected a result to be returned",
|
||||
"799": "expected a page response, got %s",
|
||||
"800": "`experimental.rdcForNavigations` is enabled, but `experimental.ppr` is not.",
|
||||
"801": "> `pages` and `app` directories should be under the same folder"
|
||||
"801": "> `pages` and `app` directories should be under the same folder",
|
||||
"802": "Image with src \"%s\" has both \"preload\" and \"priority\" properties. Only \"preload\" should be used.",
|
||||
"803": "Image with src \"%s\" has both \"preload\" and \"loading='lazy'\" properties. Only one should be used."
|
||||
}
|
||||
|
||||
@@ -409,7 +409,7 @@ export const Image = forwardRef<HTMLImageElement | null, ImageProps>(
|
||||
ref={forwardedRef}
|
||||
/>
|
||||
}
|
||||
{imgMeta.priority ? (
|
||||
{imgMeta.preload ? (
|
||||
<ImagePreload
|
||||
isAppRouter={isAppRouter}
|
||||
imgAttributes={imgAttributes}
|
||||
|
||||
@@ -35,6 +35,11 @@ export type ImageProps = Omit<
|
||||
fill?: boolean
|
||||
loader?: ImageLoader
|
||||
quality?: number | `${number}`
|
||||
preload?: boolean
|
||||
/**
|
||||
* @deprecated Use `preload` prop instead.
|
||||
* See https://nextjs.org/docs/app/api-reference/components/image#preload
|
||||
*/
|
||||
priority?: boolean
|
||||
loading?: LoadingValue
|
||||
placeholder?: PlaceholderValue
|
||||
@@ -139,7 +144,7 @@ function isStaticImport(src: string | StaticImport): src is StaticImport {
|
||||
|
||||
const allImgs = new Map<
|
||||
string,
|
||||
{ src: string; priority: boolean; placeholder: PlaceholderValue }
|
||||
{ src: string; loading: LoadingValue; placeholder: PlaceholderValue }
|
||||
>()
|
||||
let perfObserver: PerformanceObserver | undefined
|
||||
|
||||
@@ -261,6 +266,7 @@ export function getImgProps(
|
||||
sizes,
|
||||
unoptimized = false,
|
||||
priority = false,
|
||||
preload = false,
|
||||
loading,
|
||||
className,
|
||||
quality,
|
||||
@@ -292,7 +298,7 @@ export function getImgProps(
|
||||
props: ImgProps
|
||||
meta: {
|
||||
unoptimized: boolean
|
||||
priority: boolean
|
||||
preload: boolean
|
||||
placeholder: NonNullable<ImageProps['placeholder']>
|
||||
fill: boolean
|
||||
}
|
||||
@@ -408,7 +414,9 @@ export function getImgProps(
|
||||
src = typeof src === 'string' ? src : staticSrc
|
||||
|
||||
let isLazy =
|
||||
!priority && (loading === 'lazy' || typeof loading === 'undefined')
|
||||
!priority &&
|
||||
!preload &&
|
||||
(loading === 'lazy' || typeof loading === 'undefined')
|
||||
if (!src || src.startsWith('data:') || src.startsWith('blob:')) {
|
||||
// https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
|
||||
unoptimized = true
|
||||
@@ -516,6 +524,16 @@ export function getImgProps(
|
||||
`Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`
|
||||
)
|
||||
}
|
||||
if (preload && loading === 'lazy') {
|
||||
throw new Error(
|
||||
`Image with src "${src}" has both "preload" and "loading='lazy'" properties. Only one should be used.`
|
||||
)
|
||||
}
|
||||
if (preload && priority) {
|
||||
throw new Error(
|
||||
`Image with src "${src}" has both "preload" and "priority" properties. Only "preload" should be used.`
|
||||
)
|
||||
}
|
||||
if (
|
||||
placeholder !== 'empty' &&
|
||||
placeholder !== 'blur' &&
|
||||
@@ -626,15 +644,15 @@ export function getImgProps(
|
||||
const lcpImage = allImgs.get(imgSrc)
|
||||
if (
|
||||
lcpImage &&
|
||||
!lcpImage.priority &&
|
||||
lcpImage.loading === 'lazy' &&
|
||||
lcpImage.placeholder === 'empty' &&
|
||||
!lcpImage.src.startsWith('data:') &&
|
||||
!lcpImage.src.startsWith('blob:')
|
||||
) {
|
||||
// https://web.dev/lcp/#measure-lcp-in-javascript
|
||||
warnOnce(
|
||||
`Image with src "${lcpImage.src}" was detected as the Largest Contentful Paint (LCP). Please add the "priority" property if this image is above the fold.` +
|
||||
`\nRead more: https://nextjs.org/docs/api-reference/next/image#priority`
|
||||
`Image with src "${lcpImage.src}" was detected as the Largest Contentful Paint (LCP). Please add the \`loading="eager"\` property if this image is above the fold.` +
|
||||
`\nRead more: https://nextjs.org/docs/app/api-reference/components/image#loading`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -722,6 +740,8 @@ export function getImgProps(
|
||||
loader,
|
||||
})
|
||||
|
||||
const loadingFinal = isLazy ? 'lazy' : loading
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof window !== 'undefined') {
|
||||
let fullUrl: URL
|
||||
@@ -730,13 +750,13 @@ export function getImgProps(
|
||||
} catch (e) {
|
||||
fullUrl = new URL(imgAttributes.src, window.location.href)
|
||||
}
|
||||
allImgs.set(fullUrl.href, { src, priority, placeholder })
|
||||
allImgs.set(fullUrl.href, { src, loading: loadingFinal, placeholder })
|
||||
}
|
||||
}
|
||||
|
||||
const props: ImgProps = {
|
||||
...rest,
|
||||
loading: isLazy ? 'lazy' : loading,
|
||||
loading: loadingFinal,
|
||||
fetchPriority,
|
||||
width: widthInt,
|
||||
height: heightInt,
|
||||
@@ -747,6 +767,6 @@ export function getImgProps(
|
||||
srcSet: imgAttributes.srcSet,
|
||||
src: overrideSrc || imgAttributes.src,
|
||||
}
|
||||
const meta = { unoptimized, priority, placeholder, fill }
|
||||
const meta = { unoptimized, preload: preload || priority, placeholder, fill }
|
||||
return { props, meta }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>Preload Page</p>
|
||||
<Image
|
||||
preload
|
||||
id="basic-image"
|
||||
alt="basic-image"
|
||||
src="/test.jpg"
|
||||
width="400"
|
||||
height="400"
|
||||
></Image>
|
||||
<Image
|
||||
preload
|
||||
id="basic-image-crossorigin"
|
||||
alt="basic-image-crossorigin"
|
||||
src="/test.webp"
|
||||
width="400"
|
||||
height="400"
|
||||
crossOrigin="use-credentials"
|
||||
></Image>
|
||||
<Image
|
||||
preload
|
||||
id="basic-image-referrerpolicy"
|
||||
alt="basic-image-referrerpolicy"
|
||||
src="/test.png"
|
||||
width="400"
|
||||
height="400"
|
||||
referrerPolicy="no-referrer"
|
||||
></Image>
|
||||
<Image
|
||||
loading="eager"
|
||||
id="load-eager"
|
||||
alt="load-eager"
|
||||
src="/test.png"
|
||||
width="200"
|
||||
height="200"
|
||||
></Image>
|
||||
<Image
|
||||
preload
|
||||
id="responsive1"
|
||||
alt="responsive1"
|
||||
src="/wide.png"
|
||||
width="1200"
|
||||
height="700"
|
||||
sizes="100vw"
|
||||
/>
|
||||
<Image
|
||||
preload
|
||||
id="responsive2"
|
||||
alt="responsive2"
|
||||
src="/wide.png"
|
||||
width="1200"
|
||||
height="700"
|
||||
sizes="100vw"
|
||||
/>
|
||||
<Image
|
||||
id="pri-low"
|
||||
alt="pri-low"
|
||||
src="/test.webp"
|
||||
width="100"
|
||||
height="100"
|
||||
fetchPriority="low"
|
||||
/>
|
||||
<p id="stubtext">This is the preload page</p>
|
||||
<div style={{ height: '1000vh' }} />
|
||||
<Image
|
||||
preload
|
||||
id="belowthefold"
|
||||
src="/test.tiff"
|
||||
width="400"
|
||||
height="400"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -255,6 +255,157 @@ function runTests(mode: 'dev' | 'server') {
|
||||
}
|
||||
})
|
||||
|
||||
it('should work with preload prop', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/preload')
|
||||
|
||||
await check(async () => {
|
||||
const result = await browser.eval(
|
||||
`document.getElementById('basic-image').naturalWidth`
|
||||
)
|
||||
|
||||
if (result === 0) {
|
||||
throw new Error('Incorrectly loaded image')
|
||||
}
|
||||
|
||||
return 'result-correct'
|
||||
}, /result-correct/)
|
||||
|
||||
const links = await browser.elementsByCss('link[rel=preload][as=image]')
|
||||
const entries = []
|
||||
for (const link of links) {
|
||||
const fetchpriority = await link.getAttribute('fetchpriority')
|
||||
const imagesrcset = await link.getAttribute('imagesrcset')
|
||||
const imagesizes = await link.getAttribute('imagesizes')
|
||||
const crossorigin = await link.getAttribute('crossorigin')
|
||||
const referrerpolicy = await link.getAttribute('referrerPolicy')
|
||||
entries.push({
|
||||
fetchpriority,
|
||||
imagesrcset,
|
||||
imagesizes,
|
||||
crossorigin,
|
||||
referrerpolicy,
|
||||
})
|
||||
}
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_next/image?url=%2Ftest.webp&w=640&q=75 1x, /_next/image?url=%2Ftest.webp&w=828&q=75 2x'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '',
|
||||
imagesrcset:
|
||||
'/_next/image?url=%2Ftest.webp&w=640&q=75 1x, /_next/image?url=%2Ftest.webp&w=828&q=75 2x',
|
||||
crossorigin: 'use-credentials',
|
||||
referrerpolicy: '',
|
||||
})
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_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'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '100vw',
|
||||
imagesrcset:
|
||||
'/_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',
|
||||
crossorigin: '',
|
||||
referrerpolicy: '',
|
||||
})
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_next/image?url=%2Ftest.png&w=640&q=75 1x, /_next/image?url=%2Ftest.png&w=828&q=75 2x'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '',
|
||||
imagesrcset:
|
||||
'/_next/image?url=%2Ftest.png&w=640&q=75 1x, /_next/image?url=%2Ftest.png&w=828&q=75 2x',
|
||||
crossorigin: '',
|
||||
referrerpolicy: 'no-referrer',
|
||||
})
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_next/image?url=%2Ftest.tiff&w=640&q=75 1x, /_next/image?url=%2Ftest.tiff&w=828&q=75 2x'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '',
|
||||
imagesrcset:
|
||||
'/_next/image?url=%2Ftest.tiff&w=640&q=75 1x, /_next/image?url=%2Ftest.tiff&w=828&q=75 2x',
|
||||
crossorigin: '',
|
||||
referrerpolicy: '',
|
||||
})
|
||||
|
||||
// When preload={true}, we should _not_ set loading="lazy"
|
||||
expect(
|
||||
await browser.elementById('basic-image').getAttribute('loading')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('load-eager').getAttribute('loading')
|
||||
).toBe('eager')
|
||||
expect(
|
||||
await browser.elementById('responsive1').getAttribute('loading')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('responsive2').getAttribute('loading')
|
||||
).toBe(null)
|
||||
|
||||
// When preload={true}, we should not set fetchpriority="high"
|
||||
expect(
|
||||
await browser.elementById('basic-image').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('load-eager').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('responsive1').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('responsive2').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
|
||||
// Setting fetchPriority="low" directly should pass-through to <img>
|
||||
expect(
|
||||
await browser.elementById('pri-low').getAttribute('fetchpriority')
|
||||
).toBe('low')
|
||||
expect(await browser.elementById('pri-low').getAttribute('loading')).toBe(
|
||||
'lazy'
|
||||
)
|
||||
|
||||
expect(
|
||||
await browser.elementById('belowthefold').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('belowthefold').getAttribute('loading')
|
||||
).toBe(null)
|
||||
|
||||
const warnings = (await browser.log())
|
||||
.map((log) => log.message)
|
||||
.join('\n')
|
||||
expect(warnings).not.toMatch(
|
||||
/was detected as the Largest Contentful Paint/gm
|
||||
)
|
||||
expect(warnings).not.toMatch(/React does not recognize the (.+) prop/gm)
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should not pass through user-provided srcset (causing a flash)', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/drop-srcset')
|
||||
const $html = cheerio.load(html)
|
||||
@@ -1077,8 +1228,8 @@ function runTests(mode: 'dev' | 'server') {
|
||||
expect(warnings).not.toMatch(/cannot appear as a descendant/gm)
|
||||
})
|
||||
|
||||
it('should warn when priority prop is missing on LCP image', async () => {
|
||||
let browser = await webdriver(appPort, '/priority-missing-warning')
|
||||
it('should warn when preload prop is missing on LCP image', async () => {
|
||||
let browser = await webdriver(appPort, '/preload-missing-warning')
|
||||
try {
|
||||
// Wait for image to load:
|
||||
await check(async () => {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>Preload Page</p>
|
||||
<Image
|
||||
preload
|
||||
id="basic-image"
|
||||
alt="basic-image"
|
||||
src="/test.jpg"
|
||||
width="400"
|
||||
height="400"
|
||||
></Image>
|
||||
<Image
|
||||
preload
|
||||
id="basic-image-crossorigin"
|
||||
alt="basic-image-crossorigin"
|
||||
src="/test.webp"
|
||||
width="400"
|
||||
height="400"
|
||||
crossOrigin="use-credentials"
|
||||
></Image>
|
||||
<Image
|
||||
preload
|
||||
id="basic-image-referrerpolicy"
|
||||
alt="basic-image-referrerpolicy"
|
||||
src="/test.png"
|
||||
width="400"
|
||||
height="400"
|
||||
referrerPolicy="no-referrer"
|
||||
></Image>
|
||||
<Image
|
||||
loading="eager"
|
||||
id="load-eager"
|
||||
alt="load-eager"
|
||||
src="/test.png"
|
||||
width="200"
|
||||
height="200"
|
||||
></Image>
|
||||
<Image
|
||||
preload
|
||||
id="responsive1"
|
||||
alt="responsive1"
|
||||
src="/wide.png"
|
||||
width="1200"
|
||||
height="700"
|
||||
sizes="100vw"
|
||||
/>
|
||||
<Image
|
||||
preload
|
||||
id="responsive2"
|
||||
alt="responsive2"
|
||||
src="/wide.png"
|
||||
width="1200"
|
||||
height="700"
|
||||
sizes="100vw"
|
||||
/>
|
||||
<Image
|
||||
id="pri-low"
|
||||
alt="pri-low"
|
||||
src="/test.webp"
|
||||
width="100"
|
||||
height="100"
|
||||
fetchPriority="low"
|
||||
/>
|
||||
<p id="stubtext">This is the preload page</p>
|
||||
<div style={{ height: '1000vh' }} />
|
||||
<Image
|
||||
preload
|
||||
id="belowthefold"
|
||||
src="/test.tiff"
|
||||
width="400"
|
||||
height="400"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -257,6 +257,157 @@ function runTests(mode) {
|
||||
}
|
||||
})
|
||||
|
||||
it('should work with preload prop', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/preload')
|
||||
|
||||
await check(async () => {
|
||||
const result = await browser.eval(
|
||||
`document.getElementById('basic-image').naturalWidth`
|
||||
)
|
||||
|
||||
if (result === 0) {
|
||||
throw new Error('Incorrectly loaded image')
|
||||
}
|
||||
|
||||
return 'result-correct'
|
||||
}, /result-correct/)
|
||||
|
||||
const links = await browser.elementsByCss('link[rel=preload][as=image]')
|
||||
const entries = []
|
||||
for (const link of links) {
|
||||
const fetchpriority = await link.getAttribute('fetchpriority')
|
||||
const imagesrcset = await link.getAttribute('imagesrcset')
|
||||
const imagesizes = await link.getAttribute('imagesizes')
|
||||
const crossorigin = await link.getAttribute('crossorigin')
|
||||
const referrerpolicy = await link.getAttribute('referrerPolicy')
|
||||
entries.push({
|
||||
fetchpriority,
|
||||
imagesrcset,
|
||||
imagesizes,
|
||||
crossorigin,
|
||||
referrerpolicy,
|
||||
})
|
||||
}
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_next/image?url=%2Ftest.webp&w=640&q=75 1x, /_next/image?url=%2Ftest.webp&w=828&q=75 2x'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '',
|
||||
imagesrcset:
|
||||
'/_next/image?url=%2Ftest.webp&w=640&q=75 1x, /_next/image?url=%2Ftest.webp&w=828&q=75 2x',
|
||||
crossorigin: 'use-credentials',
|
||||
referrerpolicy: '',
|
||||
})
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_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'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '100vw',
|
||||
imagesrcset:
|
||||
'/_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',
|
||||
crossorigin: '',
|
||||
referrerpolicy: '',
|
||||
})
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_next/image?url=%2Ftest.png&w=640&q=75 1x, /_next/image?url=%2Ftest.png&w=828&q=75 2x'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '',
|
||||
imagesrcset:
|
||||
'/_next/image?url=%2Ftest.png&w=640&q=75 1x, /_next/image?url=%2Ftest.png&w=828&q=75 2x',
|
||||
crossorigin: '',
|
||||
referrerpolicy: 'no-referrer',
|
||||
})
|
||||
|
||||
expect(
|
||||
entries.find(
|
||||
(item) =>
|
||||
item.imagesrcset ===
|
||||
'/_next/image?url=%2Ftest.tiff&w=640&q=75 1x, /_next/image?url=%2Ftest.tiff&w=828&q=75 2x'
|
||||
)
|
||||
).toEqual({
|
||||
fetchpriority: '',
|
||||
imagesizes: '',
|
||||
imagesrcset:
|
||||
'/_next/image?url=%2Ftest.tiff&w=640&q=75 1x, /_next/image?url=%2Ftest.tiff&w=828&q=75 2x',
|
||||
crossorigin: '',
|
||||
referrerpolicy: '',
|
||||
})
|
||||
|
||||
// When preload={true}, we should _not_ set loading="lazy"
|
||||
expect(
|
||||
await browser.elementById('basic-image').getAttribute('loading')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('load-eager').getAttribute('loading')
|
||||
).toBe('eager')
|
||||
expect(
|
||||
await browser.elementById('responsive1').getAttribute('loading')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('responsive2').getAttribute('loading')
|
||||
).toBe(null)
|
||||
|
||||
// When preload={true}, we not should set fetchpriority="high"
|
||||
expect(
|
||||
await browser.elementById('basic-image').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('load-eager').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('responsive1').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('responsive2').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
|
||||
// Setting fetchPriority="low" directly should pass-through to <img>
|
||||
expect(
|
||||
await browser.elementById('pri-low').getAttribute('fetchpriority')
|
||||
).toBe('low')
|
||||
expect(await browser.elementById('pri-low').getAttribute('loading')).toBe(
|
||||
'lazy'
|
||||
)
|
||||
|
||||
expect(
|
||||
await browser.elementById('belowthefold').getAttribute('fetchpriority')
|
||||
).toBe(null)
|
||||
expect(
|
||||
await browser.elementById('belowthefold').getAttribute('loading')
|
||||
).toBe(null)
|
||||
|
||||
const warnings = (await browser.log())
|
||||
.map((log) => log.message)
|
||||
.join('\n')
|
||||
expect(warnings).not.toMatch(
|
||||
/was detected as the Largest Contentful Paint/gm
|
||||
)
|
||||
expect(warnings).not.toMatch(/React does not recognize the (.+) prop/gm)
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should not pass through user-provided srcset (causing a flash)', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/drop-srcset')
|
||||
const $html = cheerio.load(html)
|
||||
@@ -1079,8 +1230,8 @@ function runTests(mode) {
|
||||
expect(warnings).not.toMatch(/cannot appear as a descendant/gm)
|
||||
})
|
||||
|
||||
it('should warn when priority prop is missing on LCP image', async () => {
|
||||
let browser = await webdriver(appPort, '/priority-missing-warning')
|
||||
it('should warn when preload prop is missing on LCP image', async () => {
|
||||
let browser = await webdriver(appPort, '/preload-missing-warning')
|
||||
try {
|
||||
// Wait for image to load:
|
||||
await check(async () => {
|
||||
|
||||
@@ -61,6 +61,47 @@ describe('getImageProps()', () => {
|
||||
expect(props.srcSet).toBeString()
|
||||
})
|
||||
|
||||
it('should handle preload', async () => {
|
||||
const { props } = getImageProps({
|
||||
alt: 'a nice desc',
|
||||
id: 'my-image',
|
||||
src: '/test.png',
|
||||
width: 100,
|
||||
height: 200,
|
||||
preload: true,
|
||||
})
|
||||
expect(warningMessages).toStrictEqual([])
|
||||
expect(Object.entries(props)).toStrictEqual([
|
||||
['alt', 'a nice desc'],
|
||||
['id', 'my-image'],
|
||||
['width', 100],
|
||||
['height', 200],
|
||||
['decoding', 'async'],
|
||||
['style', { color: 'transparent' }],
|
||||
[
|
||||
'srcSet',
|
||||
'/_next/image?url=%2Ftest.png&w=128&q=75 1x, /_next/image?url=%2Ftest.png&w=256&q=75 2x',
|
||||
],
|
||||
['src', '/_next/image?url=%2Ftest.png&w=256&q=75'],
|
||||
])
|
||||
})
|
||||
|
||||
it('should error when both priority and preload are used', async () => {
|
||||
expect(() =>
|
||||
getImageProps({
|
||||
alt: 'a nice desc',
|
||||
id: 'my-image',
|
||||
src: '/test.png',
|
||||
width: 100,
|
||||
height: 200,
|
||||
preload: true,
|
||||
priority: true,
|
||||
})
|
||||
).toThrow(
|
||||
'Image with src "/test.png" has both "preload" and "priority" properties. Only "preload" should be used.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle priority', async () => {
|
||||
const { props } = getImageProps({
|
||||
alt: 'a nice desc',
|
||||
|
||||
Reference in New Issue
Block a user