diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index cf7310e0e651f69..acd8eed4532cb89 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -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`. diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index e45b212f5debb15..bd6267c378796be 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -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 ( + <> +

My Homepage

+ Picture of the author +

Welcome to my homepage!

+ + ) +} +``` + +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. diff --git a/docs/upgrading.md b/docs/upgrading.md index dd0a4bd8d8a4e17..ad40d71a1c12fbe 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -38,6 +38,12 @@ module.exports = { Minification using SWC is an opt-in flag to ensure it can be tested against more real-world Next.js applications before it becomes the default in Next.js 12.1. If you have feedback about minification, please leave it on [the feedback thread](https://github.com/vercel/next.js/discussions/30237). +### Improvements to styled-jsx CSS parsing + +On top of the Rust-based compiler we've implemented a new CSS parser based on the CSS parser that was used for the styled-jsx Babel transform. This new parser has improved handling of CSS and now errors when invalid CSS is used that would previously slip through and cause unexpected behavior. + +Because of this change invalid CSS will throw an error during development and `next build`. This change only affects styled-jsx usage. + ### `next/image` changed wrapping element `next/image` now renders the `` inside a `` instead of `
`. diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 9c4a2ae7a6d744c..83dbf6d474e98e2 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -10,6 +10,11 @@ import { import { useIntersection } from './use-intersection' const loadedImageURLs = new Set() +const allImgs = new Map< + string, + { src: string; priority: boolean; placeholder: string } +>() +let perfObserver: PerformanceObserver | undefined const emptyDataURL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' @@ -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({ @@ -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 ( {hasSizer ? ( diff --git a/test/integration/image-component/default/pages/priority-missing-warning.js b/test/integration/image-component/default/pages/priority-missing-warning.js new file mode 100644 index 000000000000000..9ad9b0db730ce01 --- /dev/null +++ b/test/integration/image-component/default/pages/priority-missing-warning.js @@ -0,0 +1,27 @@ +import React from 'react' +import Image from 'next/image' + +const Page = () => { + return ( +
+

Priority Missing Warning Page

+ + + +
+ ) +} + +export default Page diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js index c3c21dc5af424af..4998da538db1528 100644 --- a/test/integration/image-component/default/test/index.test.js +++ b/test/integration/image-component/default/test/index.test.js @@ -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() @@ -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 () => {