From 72f127d08595716015d35404db73c3b16efe26e2 Mon Sep 17 00:00:00 2001 From: atcastle Date: Thu, 5 Nov 2020 13:37:28 -0800 Subject: [PATCH 1/3] Move CSS Preloads to top of head at document render --- packages/next/client/image.tsx | 4 +--- packages/next/pages/_document.tsx | 17 ++++++++++++++++ .../image-component/basic/pages/index.js | 5 +++++ .../image-component/basic/public/styles.css | 3 +++ .../image-component/basic/test/index.test.js | 20 +++++++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 test/integration/image-component/basic/public/styles.css diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index fca61bdded78f..a3e34b571986b 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -468,9 +468,7 @@ export default function Image({ 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' + const shouldPreload = priority if (unsized) { wrapperStyle = undefined diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 4b0334d927835..344e0cc24b051 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -314,6 +314,23 @@ export class Head extends Component< this.context.docComponentsRendered.Head = true let { head } = this.context + let cssPreloads: Array = [] + let otherHeadElements: Array = [] + if (head) { + head.forEach((c) => { + if ( + c && + c.type === 'link' && + c.props['rel'] === 'preload' && + c.props['as'] === 'style' + ) { + cssPreloads.push(c) + } else { + c && otherHeadElements.push(c) + } + }) + head = cssPreloads.concat(otherHeadElements) + } let children = this.props.children // show a warning if Head contains (only in development) if (process.env.NODE_ENV !== 'production') { diff --git a/test/integration/image-component/basic/pages/index.js b/test/integration/image-component/basic/pages/index.js index c2f939b1acbf1..db07560c03ec0 100644 --- a/test/integration/image-component/basic/pages/index.js +++ b/test/integration/image-component/basic/pages/index.js @@ -1,6 +1,7 @@ import React from 'react' import Image from 'next/image' import Link from 'next/link' +import Head from 'next/head' const Page = () => { return ( @@ -90,6 +91,10 @@ const Page = () => { <Link href="/lazy"> <a id="lazylink">lazy</a> </Link> + <Head> + <link rel="stylesheet" href="styles.css" /> + <link rel="preload" href="styles.css" as="style" /> + </Head> <p id="stubtext">This is the index page</p> </div> ) diff --git a/test/integration/image-component/basic/public/styles.css b/test/integration/image-component/basic/public/styles.css new file mode 100644 index 0000000000000..3d9a2b2a48459 --- /dev/null +++ b/test/integration/image-component/basic/public/styles.css @@ -0,0 +1,3 @@ +p { + color: red; +} diff --git a/test/integration/image-component/basic/test/index.test.js b/test/integration/image-component/basic/test/index.test.js index f637d729c3c4a..822f3c7f73936 100644 --- a/test/integration/image-component/basic/test/index.test.js +++ b/test/integration/image-component/basic/test/index.test.js @@ -202,6 +202,23 @@ async function hasPreloadLinkMatchingUrl(url) { return foundMatch } +async function hasImagePreloadBeforeCSSPreload() { + const links = await browser.elementsByCss('link') + let foundImage = false + for (const link of links) { + const rel = await link.getAttribute('rel') + if (rel === 'preload') { + const linkAs = await link.getAttribute('as') + if (linkAs === 'image') { + foundImage = true + } else if (linkAs === 'style' && foundImage) { + return true + } + } + } + return false +} + describe('Image Component Tests', () => { beforeAll(async () => { await nextBuild(appDir) @@ -245,6 +262,9 @@ describe('Image Component Tests', () => { ) ).toBe(true) }) + it('should not create any preload tags higher up the page than CSS preload tags', async () => { + expect(await hasImagePreloadBeforeCSSPreload()).toBe(false) + }) }) describe('Client-side Image Component Tests', () => { beforeAll(async () => { From 69f848af575b0f15c8961082d3f15d2038fed0ad Mon Sep 17 00:00:00 2001 From: Joe Haddad <joe.haddad@zeit.co> Date: Wed, 30 Dec 2020 12:23:19 -0500 Subject: [PATCH 2/3] Restore image.tsx --- packages/next/client/image.tsx | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index a3e34b571986b..12a48c7a5ab96 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -30,6 +30,8 @@ type ImageData = { domains?: string[] } +type ImgElementStyle = NonNullable<JSX.IntrinsicElements['img']['style']> + type ImageProps = Omit< JSX.IntrinsicElements['img'], 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style' @@ -39,6 +41,8 @@ type ImageProps = Omit< priority?: boolean loading?: LoadingValue unoptimized?: boolean + objectFit?: ImgElementStyle['objectFit'] + objectPosition?: ImgElementStyle['objectPosition'] } & ( | { width?: never @@ -247,6 +251,8 @@ export default function Image({ quality, width, height, + objectFit, + objectPosition, ...all }: ImageProps) { const thisEl = useRef<HTMLImageElement>(null) @@ -310,6 +316,12 @@ export default function Image({ lazy = false } + if (src && src.startsWith('data:')) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs + unoptimized = true + lazy = false + } + useEffect(() => { const target = thisEl.current @@ -336,7 +348,7 @@ export default function Image({ let wrapperStyle: JSX.IntrinsicElements['div']['style'] | undefined let sizerStyle: JSX.IntrinsicElements['div']['style'] | undefined let sizerSvg: string | undefined - let imgStyle: JSX.IntrinsicElements['img']['style'] = { + let imgStyle: ImgElementStyle | undefined = { visibility: lazy ? 'hidden' : 'visible', position: 'absolute', @@ -357,6 +369,9 @@ export default function Image({ maxWidth: '100%', minHeight: '100%', maxHeight: '100%', + + objectFit, + objectPosition, } if ( typeof widthInt !== 'undefined' && @@ -468,7 +483,9 @@ export default function Image({ className = className ? className + ' __lazy' : '__lazy' } - const shouldPreload = priority + // 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' if (unsized) { wrapperStyle = undefined @@ -570,14 +587,20 @@ function defaultLoader({ root, src, width, quality }: LoaderProps): string { ) } - if (src && !src.startsWith('/') && configDomains) { + if (src.startsWith('//')) { + throw new Error( + `Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)` + ) + } + + if (!src.startsWith('/') && configDomains) { let parsedSrc: URL try { parsedSrc = new URL(src) } catch (err) { console.error(err) throw new Error( - `Failed to parse "${src}" in "next/image", if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)` + `Failed to parse src "${src}" on \`next/image\`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)` ) } From 2d6ef79410d4ddddde276386d595af54e68bf3b2 Mon Sep 17 00:00:00 2001 From: Joe Haddad <joe.haddad@zeit.co> Date: Wed, 30 Dec 2020 12:23:46 -0500 Subject: [PATCH 3/3] Restore image.tsx