Skip to content

Commit

Permalink
Merge branch 'canary' into mutate
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] committed Oct 25, 2021
2 parents d88784a + 4782cac commit f513556
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/api-reference/next/image.md
Expand Up @@ -134,7 +134,7 @@ The quality of the optimized image, an integer between `1` and `100` where `100`
When true, the image will be considered high priority and
[preload](https://web.dev/preload-responsive-images/). Lazy loading is automatically disabled for images using `priority`.

You should use the `priority` attribute on any image which you suspect will be the [Largest Contentful Paint (LCP) element](https://nextjs.org/learn/seo/web-performance/lcp). It may be appropriate to have multiple priority images, as different images may be the LCP element for different viewport sizes.
You should use the `priority` property on any image detected as the [Largest Contentful Paint (LCP)](https://nextjs.org/learn/seo/web-performance/lcp) element. It may be appropriate to have multiple priority images, as different images may be the LCP element for different viewport sizes.

Should only be used when the image is visible above the fold. Defaults to `false`.

Expand Down
38 changes: 38 additions & 0 deletions docs/basic-features/image-optimization.md
Expand Up @@ -113,6 +113,44 @@ The default loader for Next.js applications uses the built-in Image Optimization

Loaders can be defined per-image, or at the application level.

### Priority

You should add the `priority` property to the image that will be the [Largest Contentful Paint (LCP) element](https://web.dev/lcp/#what-elements-are-considered) for each page. Doing so allows Next.js to specially prioritize the image for loading (e.g. through preload tags or priority hints), leading to a meaningful boost in LCP.

The LCP element is typically the largest image or text block visible within the viewport of the page. You can verify which element this is by running the following code in the console of your page and looking at the latest result:

```javascript
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP candidate:', entry.startTime, entry.element)
}
}).observe({ type: 'largest-contentful-paint', buffered: true })
```

Once you've identified the LCP image, you can add the property like this:

```jsx
import Image from 'next/image'

export default function Home() {
return (
<>
<h1>My Homepage</h1>
<Image
src="/me.png"
alt="Picture of the author"
width={500}
height={500}
priority
/>
<p>Welcome to my homepage!</p>
</>
)
}
```

See more about priority in the [`next/image` component documentation](/docs/api-reference/next/image.md#priority).

## Image Sizing

One of the ways that images most commonly hurt performance is through _layout shift_, where the image pushes other elements around on the page as it loads in. This performance problem is so annoying to users that it has its own Core Web Vital, called [Cumulative Layout Shift](https://web.dev/cls/). The way to avoid image-based layout shifts is to [always size your images](https://web.dev/optimize-cls/#images-without-dimensions). This allows the browser to reserve precisely enough space for the image before it loads.
Expand Down
41 changes: 41 additions & 0 deletions packages/next/client/image.tsx
Expand Up @@ -10,6 +10,11 @@ import {
import { useIntersection } from './use-intersection'

const loadedImageURLs = new Set<string>()
const allImgs = new Map<
string,
{ src: string; priority: boolean; placeholder: string }
>()
let perfObserver: PerformanceObserver | undefined
const emptyDataURL =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

Expand Down Expand Up @@ -450,6 +455,30 @@ export default function Image({
`\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader-width`
)
}

if (typeof window !== 'undefined' && !perfObserver) {
perfObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// @ts-ignore - missing "LargestContentfulPaint" class with "element" prop
const imgSrc = entry?.element?.src || ''
const lcpImage = allImgs.get(imgSrc)
if (
lcpImage &&
!lcpImage.priority &&
lcpImage.placeholder !== 'blur' &&
!lcpImage.src.startsWith('data:') &&
!lcpImage.src.startsWith('blob:')
) {
// https://web.dev/lcp/#measure-lcp-in-javascript
console.warn(
`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`
)
}
}
})
perfObserver.observe({ type: 'largest-contentful-paint', buffered: true })
}
}

const [setRef, isIntersected] = useIntersection<HTMLImageElement>({
Expand Down Expand Up @@ -580,6 +609,18 @@ export default function Image({

let srcString: string = src

if (process.env.NODE_ENV !== 'production') {
if (typeof window !== 'undefined') {
let fullUrl: URL
try {
fullUrl = new URL(imgAttributes.src)
} catch (e) {
fullUrl = new URL(imgAttributes.src, window.location.href)
}
allImgs.set(fullUrl.href, { src, priority, placeholder })
}
}

return (
<span style={wrapperStyle}>
{hasSizer ? (
Expand Down
@@ -0,0 +1,27 @@
import React from 'react'
import Image from 'next/image'

const Page = () => {
return (
<div>
<h1>Priority Missing Warning Page</h1>
<Image
id="responsive"
layout="responsive"
src="/wide.png"
width="1200"
height="700"
/>
<Image
id="fixed"
layout="fixed"
src="/test.jpg"
width="400"
height="400"
/>
<footer>Priority Missing Warning Footer</footer>
</div>
)
}

export default Page
36 changes: 36 additions & 0 deletions test/integration/image-component/default/test/index.test.js
Expand Up @@ -135,6 +135,13 @@ function runTests(mode) {
'/_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',
},
])

const warnings = (await browser.log('browser'))
.map((log) => log.message)
.join('\n')
expect(warnings).not.toMatch(
/was detected as the Largest Contentful Paint/gm
)
} finally {
if (browser) {
await browser.close()
Expand Down Expand Up @@ -658,6 +665,35 @@ 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
try {
browser = await webdriver(appPort, '/priority-missing-warning')
// Wait for image to load:
await check(async () => {
const result = await browser.eval(
`document.getElementById('responsive').naturalWidth`
)
if (result < 1) {
throw new Error('Image not ready')
}
return 'done'
}, 'done')
await waitFor(1000)
const warnings = (await browser.log('browser'))
.map((log) => log.message)
.join('\n')
expect(await hasRedbox(browser)).toBe(false)
expect(warnings).toMatch(
/Image with src (.*)wide.png(.*) was detected as the Largest Contentful Paint/gm
)
} finally {
if (browser) {
await browser.close()
}
}
})
} else {
//server-only tests
it('should not create an image folder in server/chunks', async () => {
Expand Down

0 comments on commit f513556

Please sign in to comment.