From aa083de7da80a590c2f4b7746e5a1e35e629dff0 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Oct 2021 21:30:37 -0400 Subject: [PATCH 1/4] Add warning when LCP image is missing `priority` prop --- docs/api-reference/next/image.md | 2 +- packages/next/client/image.tsx | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) 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/packages/next/client/image.tsx b/packages/next/client/image.tsx index 9c4a2ae7a6d744c..214d5a4f862cfa0 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -10,6 +10,8 @@ import { import { useIntersection } from './use-intersection' const loadedImageURLs = new Set() +const allImgs = new Map() +let perfObserver: PerformanceObserver | undefined const emptyDataURL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' @@ -450,6 +452,29 @@ 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 "url" prop + const entryUrl = entry.url as string + const lcpImage = allImgs.get(entryUrl) + if ( + lcpImage && + !lcpImage.priority && + !entryUrl.startsWith('data:') && + !entryUrl.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). Consider adding the "priority" property if its 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 +605,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 }) + } + } + return ( {hasSizer ? ( From 615ebca57f90dca435f94e7a1766c495c1642b56 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Oct 2021 22:36:14 -0400 Subject: [PATCH 2/4] Add test --- packages/next/client/image.tsx | 2 +- .../default/pages/priority-missing-warning.js | 27 ++++++++++++++ .../default/test/index.test.js | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 test/integration/image-component/default/pages/priority-missing-warning.js diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 214d5a4f862cfa0..f5d54bd5934e179 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -467,7 +467,7 @@ export default function Image({ ) { // https://web.dev/lcp/#measure-lcp-in-javascript console.warn( - `Image with src "${lcpImage.src}" was detected as the Largest Contentful Paint (LCP). Consider adding the "priority" property if its above the fold.` + + `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` ) } 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

+ + +
Priority Missing Warning Footer
+
+ ) +} + +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 () => { From 3e2032ebd663902b5df96644cbe6752ef38434c8 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Oct 2021 22:40:11 -0400 Subject: [PATCH 3/4] Ignore blur placeholders --- packages/next/client/image.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index f5d54bd5934e179..70ec8eeae98ccd2 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -10,7 +10,10 @@ import { import { useIntersection } from './use-intersection' const loadedImageURLs = new Set() -const allImgs = new Map() +const allImgs = new Map< + string, + { src: string; priority: boolean; placeholder: string } +>() let perfObserver: PerformanceObserver | undefined const emptyDataURL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' @@ -462,6 +465,7 @@ export default function Image({ if ( lcpImage && !lcpImage.priority && + lcpImage.placeholder !== 'blur' && !entryUrl.startsWith('data:') && !entryUrl.startsWith('blob:') ) { @@ -613,7 +617,7 @@ export default function Image({ } catch (e) { fullUrl = new URL(imgAttributes.src, window.location.href) } - allImgs.set(fullUrl.href, { src, priority }) + allImgs.set(fullUrl.href, { src, priority, placeholder }) } } From 4ab841196522d6e2938817f7d63f6d824996efa2 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 24 Oct 2021 15:25:19 -0400 Subject: [PATCH 4/4] Improve warning detection --- packages/next/client/image.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 70ec8eeae98ccd2..83dbf6d474e98e2 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -459,15 +459,15 @@ export default function Image({ if (typeof window !== 'undefined' && !perfObserver) { perfObserver = new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { - // @ts-ignore - missing "LargestContentfulPaint" class with "url" prop - const entryUrl = entry.url as string - const lcpImage = allImgs.get(entryUrl) + // @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' && - !entryUrl.startsWith('data:') && - !entryUrl.startsWith('blob:') + !lcpImage.src.startsWith('data:') && + !lcpImage.src.startsWith('blob:') ) { // https://web.dev/lcp/#measure-lcp-in-javascript console.warn(