-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add LazyImage component so next/image behaves more nicely with JS dis…
…abled
- Loading branch information
1 parent
3b57db3
commit 15cd732
Showing
2 changed files
with
62 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from "react" | ||
import Image, { ImageProps } from "next/image" | ||
|
||
/** | ||
* Paired with a patch of next/image, this component will use the browser-native | ||
* loading="lazy" attr instead of next/image's default backwards-compatible shim. | ||
* The default behavior doesn't work with JS disabled - the browser shows an empty | ||
* image that's a placeholder so old browsers don't grab a high-res image right away. | ||
* This component fixes that, with the downside that images won't be lazily loaded | ||
* on browsers missing loading="lazy" support (e.g. Safari as of Feb 2021, IE 11). | ||
* I like this tradeoff. | ||
* | ||
* https://caniuse.com/loading-lazy-attr | ||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading | ||
* https://nextjs.org/docs/api-reference/next/image#loading | ||
*/ | ||
export const LazyImage: React.FC<ImageProps> = (props) => ( | ||
// loading="eager" tells next/image to disable its built-in lazy loading trickery | ||
// htmlLoading is a patched-in attr that gets passed to the <img> element as "loading" | ||
<Image {...props} loading="eager" htmlLoading="lazy" /> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
diff --git a/node_modules/next/dist/client/image.d.ts b/node_modules/next/dist/client/image.d.ts | ||
index 2754cbd..ff5cbfa 100644 | ||
--- a/node_modules/next/dist/client/image.d.ts | ||
+++ b/node_modules/next/dist/client/image.d.ts | ||
@@ -16,6 +16,7 @@ export declare type ImageProps = Omit<JSX.IntrinsicElements['img'], 'src' | 'src | ||
quality?: number | string; | ||
priority?: boolean; | ||
loading?: LoadingValue; | ||
+ htmlLoading?: JSX.IntrinsicElements['img']['loading']; | ||
unoptimized?: boolean; | ||
objectFit?: ImgElementStyle['objectFit']; | ||
objectPosition?: ImgElementStyle['objectPosition']; | ||
@@ -33,5 +34,5 @@ export declare type ImageProps = Omit<JSX.IntrinsicElements['img'], 'src' | 'src | ||
height: number | string; | ||
layout?: Exclude<LayoutValue, 'fill'>; | ||
}); | ||
-export default function Image({ src, sizes, unoptimized, priority, loading, className, quality, width, height, objectFit, objectPosition, loader, ...all }: ImageProps): JSX.Element; | ||
+export default function Image({ src, sizes, unoptimized, priority, loading, htmlLoading, className, quality, width, height, objectFit, objectPosition, loader, ...all }: ImageProps): JSX.Element; | ||
export {}; | ||
diff --git a/node_modules/next/dist/client/image.js b/node_modules/next/dist/client/image.js | ||
index ce8b0a6..16dae02 100644 | ||
--- a/node_modules/next/dist/client/image.js | ||
+++ b/node_modules/next/dist/client/image.js | ||
@@ -7,7 +7,7 @@ const allSizes=[...configDeviceSizes,...configImageSizes];configDeviceSizes.sort | ||
// > wasteful as the human eye cannot see that level of detail without | ||
// > something like a magnifying glass. | ||
// https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html | ||
-[width,width*2/*, width * 3*/].map(w=>allSizes.find(p=>p>=w)||allSizes[allSizes.length-1]))];return{widths,kind:'x'};}function generateImgAttrs({src,unoptimized,layout,width,quality,sizes,loader}){if(unoptimized){return{src,srcSet:undefined,sizes:undefined};}const{widths,kind}=getWidths(width,layout);const last=widths.length-1;return{src:loader({src,quality,width:widths[last]}),sizes:!sizes&&kind==='w'?'100vw':sizes,srcSet:widths.map((w,i)=>`${loader({src,quality,width:w})} ${kind==='w'?w:i+1}${kind}`).join(', ')};}function getInt(x){if(typeof x==='number'){return x;}if(typeof x==='string'){return parseInt(x,10);}return undefined;}function defaultImageLoader(loaderProps){const load=loaders.get(configLoader);if(load){return load((0,_extends2.default)({root:configPath},loaderProps));}throw new Error(`Unknown "loader" found in "next.config.js". Expected: ${_imageConfig.VALID_LOADERS.join(', ')}. Received: ${configLoader}`);}function Image(_ref){let{src,sizes,unoptimized=false,priority=false,loading,className,quality,width,height,objectFit,objectPosition,loader=defaultImageLoader}=_ref,all=(0,_objectWithoutPropertiesLoose2.default)(_ref,["src","sizes","unoptimized","priority","loading","className","quality","width","height","objectFit","objectPosition","loader"]);let rest=all;let layout=sizes?'responsive':'intrinsic';let unsized=false;if('unsized'in rest){unsized=Boolean(rest.unsized);// Remove property so it's not spread into image: | ||
+[width,width*2/*, width * 3*/].map(w=>allSizes.find(p=>p>=w)||allSizes[allSizes.length-1]))];return{widths,kind:'x'};}function generateImgAttrs({src,unoptimized,layout,width,quality,sizes,loader}){if(unoptimized){return{src,srcSet:undefined,sizes:undefined};}const{widths,kind}=getWidths(width,layout);const last=widths.length-1;return{src:loader({src,quality,width:widths[last]}),sizes:!sizes&&kind==='w'?'100vw':sizes,srcSet:widths.map((w,i)=>`${loader({src,quality,width:w})} ${kind==='w'?w:i+1}${kind}`).join(', ')};}function getInt(x){if(typeof x==='number'){return x;}if(typeof x==='string'){return parseInt(x,10);}return undefined;}function defaultImageLoader(loaderProps){const load=loaders.get(configLoader);if(load){return load((0,_extends2.default)({root:configPath},loaderProps));}throw new Error(`Unknown "loader" found in "next.config.js". Expected: ${_imageConfig.VALID_LOADERS.join(', ')}. Received: ${configLoader}`);}function Image(_ref){let{src,sizes,unoptimized=false,priority=false,loading,htmlLoading,className,quality,width,height,objectFit,objectPosition,loader=defaultImageLoader}=_ref,all=(0,_objectWithoutPropertiesLoose2.default)(_ref,["src","sizes","unoptimized","priority","loading","htmlLoading","className","quality","width","height","objectFit","objectPosition","loader"]);let rest=all;let layout=sizes?'responsive':'intrinsic';let unsized=false;if('unsized'in rest){unsized=Boolean(rest.unsized);// Remove property so it's not spread into image: | ||
delete rest['unsized'];}else if('layout'in rest){// Override default layout if the user specified one: | ||
if(rest.layout)layout=rest.layout;// Remove property so it's not spread into image: | ||
delete rest['layout'];}if(process.env.NODE_ENV!=='production'){if(!src){throw new Error(`Image is missing required "src" property. Make sure you pass "src" in props to the \`next/image\` component. Received: ${JSON.stringify({width,height,quality})}`);}if(!VALID_LAYOUT_VALUES.includes(layout)){throw new Error(`Image with src "${src}" has invalid "layout" property. Provided "${layout}" should be one of ${VALID_LAYOUT_VALUES.map(String).join(',')}.`);}if(!VALID_LOADING_VALUES.includes(loading)){throw new Error(`Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map(String).join(',')}.`);}if(priority&&loading==='lazy'){throw new Error(`Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`);}if(unsized){throw new Error(`Image with src "${src}" has deprecated "unsized" property, which was removed in favor of the "layout='fill'" property`);}}let isLazy=!priority&&(loading==='lazy'||typeof loading==='undefined');if(src&&src.startsWith('data:')){// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | ||
@@ -17,7 +17,7 @@ wrapperStyle={display:'block',overflow:'hidden',position:'relative',boxSizing:'b | ||
wrapperStyle={display:'inline-block',maxWidth:'100%',overflow:'hidden',position:'relative',boxSizing:'border-box',margin:0};sizerStyle={boxSizing:'border-box',display:'block',maxWidth:'100%'};sizerSvg=`<svg width="${widthInt}" height="${heightInt}" xmlns="http://www.w3.org/2000/svg" version="1.1"/>`;}else if(layout==='fixed'){// <Image src="i.png" width="100" height="100" layout="fixed" /> | ||
wrapperStyle={overflow:'hidden',boxSizing:'border-box',display:'inline-block',position:'relative',width:widthInt,height:heightInt};}}else if(typeof widthInt==='undefined'&&typeof heightInt==='undefined'&&layout==='fill'){// <Image src="i.png" layout="fill" /> | ||
wrapperStyle={display:'block',overflow:'hidden',position:'absolute',top:0,left:0,bottom:0,right:0,boxSizing:'border-box',margin:0};}else{// <Image src="i.png" /> | ||
-if(process.env.NODE_ENV!=='production'){throw new Error(`Image with src "${src}" must use "width" and "height" properties or "layout='fill'" property.`);}}let imgAttributes={src:'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',srcSet:undefined,sizes:undefined};if(isVisible){imgAttributes=generateImgAttrs({src,unoptimized,layout,width:widthInt,quality:qualityInt,sizes,loader});}if(unsized){wrapperStyle=undefined;sizerStyle=undefined;imgStyle=undefined;}return/*#__PURE__*/_react.default.createElement("div",{style:wrapperStyle},sizerStyle?/*#__PURE__*/_react.default.createElement("div",{style:sizerStyle},sizerSvg?/*#__PURE__*/_react.default.createElement("img",{style:{maxWidth:'100%',display:'block',margin:0,border:'none',padding:0},alt:"","aria-hidden":true,role:"presentation",src:`data:image/svg+xml;base64,${(0,_toBase.toBase64)(sizerSvg)}`}):null):null,/*#__PURE__*/_react.default.createElement("img",Object.assign({},rest,imgAttributes,{decoding:"async",className:className,ref:setRef,style:imgStyle})),priority?/*#__PURE__*/ // Note how we omit the `href` attribute, as it would only be relevant | ||
+if(process.env.NODE_ENV!=='production'){throw new Error(`Image with src "${src}" must use "width" and "height" properties or "layout='fill'" property.`);}}let imgAttributes={src:'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',srcSet:undefined,sizes:undefined};if(isVisible){imgAttributes=generateImgAttrs({src,unoptimized,layout,width:widthInt,quality:qualityInt,sizes,loader});}if(unsized){wrapperStyle=undefined;sizerStyle=undefined;imgStyle=undefined;}return/*#__PURE__*/_react.default.createElement("div",{style:wrapperStyle},sizerStyle?/*#__PURE__*/_react.default.createElement("div",{style:sizerStyle},sizerSvg?/*#__PURE__*/_react.default.createElement("img",{style:{maxWidth:'100%',display:'block',margin:0,border:'none',padding:0},alt:"","aria-hidden":true,role:"presentation",src:`data:image/svg+xml;base64,${(0,_toBase.toBase64)(sizerSvg)}`}):null):null,/*#__PURE__*/_react.default.createElement("img",Object.assign({},rest,imgAttributes,{decoding:"async",className:className,ref:setRef,style:imgStyle,loading:htmlLoading})),priority?/*#__PURE__*/ // Note how we omit the `href` attribute, as it would only be relevant | ||
// for browsers that do not support `imagesrcset`, and in those cases | ||
// it would likely cause the incorrect image to be preloaded. | ||
// |