Skip to content

Commit

Permalink
Add LazyImage component so next/image behaves more nicely with JS dis…
Browse files Browse the repository at this point in the history
…abled
  • Loading branch information
fracture91 committed Feb 15, 2021
1 parent 3b57db3 commit 15cd732
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 0 deletions.
21 changes: 21 additions & 0 deletions components/LazyImage.tsx
@@ -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" />
)
41 changes: 41 additions & 0 deletions patches/next+10.0.5.patch
@@ -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.
//

0 comments on commit 15cd732

Please sign in to comment.