2022-10-11 19:26:45 +02:00
'use client'
2022-09-17 00:12:59 +02:00
2022-06-24 16:56:05 +02:00
import React , {
useRef ,
useEffect ,
useCallback ,
useContext ,
useMemo ,
useState ,
} from 'react'
import Head from '../../shared/lib/head'
import {
ImageConfigComplete ,
imageConfigDefault ,
2022-10-14 03:59:22 +02:00
LoaderValue ,
VALID_LOADERS ,
2022-06-24 16:56:05 +02:00
} from '../../shared/lib/image-config'
2022-10-14 03:59:22 +02:00
import { useIntersection } from '../use-intersection'
2022-06-24 16:56:05 +02:00
import { ImageConfigContext } from '../../shared/lib/image-config-context'
2022-10-31 18:50:35 +01:00
import { warnOnce } from '../../shared/lib/utils/warn-once'
2022-10-14 03:59:22 +02:00
import { normalizePathTrailingSlash } from '../normalize-trailing-slash'
function normalizeSrc ( src : string ) : string {
return src [ 0 ] === '/' ? src . slice ( 1 ) : src
}
2022-06-24 16:56:05 +02:00
const configEnv = process . env . __NEXT_IMAGE_OPTS as any as ImageConfigComplete
2022-10-14 03:59:22 +02:00
const loadedImageURLs = new Set < string > ( )
2022-06-24 16:56:05 +02:00
const allImgs = new Map <
string ,
{ src : string ; priority : boolean ; placeholder : string }
> ( )
let perfObserver : PerformanceObserver | undefined
2022-10-14 03:59:22 +02:00
const emptyDataURL =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
2022-06-24 16:56:05 +02:00
if ( typeof window === 'undefined' ) {
; ( global as any ) . __NEXT_IMAGE_IMPORTED = true
}
const VALID_LOADING_VALUES = [ 'lazy' , 'eager' , undefined ] as const
type LoadingValue = typeof VALID_LOADING_VALUES [ number ]
type ImageConfig = ImageConfigComplete & { allSizes : number [ ] }
2022-10-14 03:59:22 +02:00
export type ImageLoader = ( resolverProps : ImageLoaderProps ) = > string
2022-06-24 16:56:05 +02:00
export type ImageLoaderProps = {
src : string
width : number
quality? : number
}
// Do not export - this is an internal type only
// because `next.config.js` is only meant for the
// built-in loaders, not for a custom loader() prop.
2022-10-14 03:59:22 +02:00
type ImageLoaderWithConfig = (
resolverProps : ImageLoaderPropsWithConfig
) = > string
2022-06-24 16:56:05 +02:00
type ImageLoaderPropsWithConfig = ImageLoaderProps & {
config : Readonly < ImageConfig >
}
2022-10-14 03:59:22 +02:00
function imgixLoader ( {
config ,
src ,
width ,
quality ,
} : ImageLoaderPropsWithConfig ) : string {
// Demo: https://static.imgix.net/daisy.png?auto=format&fit=max&w=300
const url = new URL ( ` ${ config . path } ${ normalizeSrc ( src ) } ` )
const params = url . searchParams
// auto params can be combined with comma separation, or reiteration
params . set ( 'auto' , params . getAll ( 'auto' ) . join ( ',' ) || 'format' )
params . set ( 'fit' , params . get ( 'fit' ) || 'max' )
params . set ( 'w' , params . get ( 'w' ) || width . toString ( ) )
if ( quality ) {
params . set ( 'q' , quality . toString ( ) )
}
return url . href
}
function akamaiLoader ( {
config ,
src ,
width ,
} : ImageLoaderPropsWithConfig ) : string {
return ` ${ config . path } ${ normalizeSrc ( src ) } ?imwidth= ${ width } `
}
function cloudinaryLoader ( {
config ,
src ,
width ,
quality ,
} : ImageLoaderPropsWithConfig ) : string {
// Demo: https://res.cloudinary.com/demo/image/upload/w_300,c_limit,q_auto/turtles.jpg
const params = [ 'f_auto' , 'c_limit' , 'w_' + width , 'q_' + ( quality || 'auto' ) ]
const paramsString = params . join ( ',' ) + '/'
return ` ${ config . path } ${ paramsString } ${ normalizeSrc ( src ) } `
}
function customLoader ( { src } : ImageLoaderProps ) : string {
throw new Error (
` Image with src " ${ src } " is missing "loader" prop. ` +
` \ nRead more: https://nextjs.org/docs/messages/next-image-missing-loader `
)
}
function defaultLoader ( {
config ,
src ,
width ,
quality ,
} : ImageLoaderPropsWithConfig ) : string {
if ( process . env . NODE_ENV !== 'production' ) {
const missingValues = [ ]
// these should always be provided but make sure they are
if ( ! src ) missingValues . push ( 'src' )
if ( ! width ) missingValues . push ( 'width' )
if ( missingValues . length > 0 ) {
throw new Error (
` Next Image Optimization requires ${ missingValues . join (
', '
) } to be provided . Make sure you pass them as props to the \ ` next/image \` component. Received: ${ JSON . stringify (
{ src , width , quality }
) } `
)
}
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 ( '/' ) && ( config . domains || config . remotePatterns ) ) {
let parsedSrc : URL
try {
parsedSrc = new URL ( src )
} catch ( err ) {
console . error ( err )
throw new Error (
` 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://) `
)
}
if ( process . env . NODE_ENV !== 'test' ) {
// We use dynamic require because this should only error in development
const { hasMatch } = require ( '../../shared/lib/match-remote-pattern' )
if ( ! hasMatch ( config . domains , config . remotePatterns , parsedSrc ) ) {
throw new Error (
` Invalid src prop ( ${ src } ) on \` next/image \` , hostname " ${ parsedSrc . hostname } " is not configured under images in your \` next.config.js \` \ n ` +
` See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host `
)
}
}
}
}
if ( src . endsWith ( '.svg' ) && ! config . dangerouslyAllowSVG ) {
// Special case to make svg serve as-is to avoid proxying
// through the built-in Image Optimization API.
return src
}
return ` ${ normalizePathTrailingSlash ( config . path ) } ?url= ${ encodeURIComponent (
src
) } & w = $ { width } & q = $ { quality || 75 } `
}
const loaders = new Map <
LoaderValue ,
( props : ImageLoaderPropsWithConfig ) = > string
> ( [
[ 'default' , defaultLoader ] ,
[ 'imgix' , imgixLoader ] ,
[ 'cloudinary' , cloudinaryLoader ] ,
[ 'akamai' , akamaiLoader ] ,
[ 'custom' , customLoader ] ,
] )
const VALID_LAYOUT_VALUES = [
'fill' ,
'fixed' ,
'intrinsic' ,
'responsive' ,
undefined ,
] as const
type LayoutValue = typeof VALID_LAYOUT_VALUES [ number ]
2022-06-24 16:56:05 +02:00
type PlaceholderValue = 'blur' | 'empty'
2022-10-14 03:59:22 +02:00
type OnLoadingComplete = ( result : {
naturalWidth : number
naturalHeight : number
} ) = > void
2022-06-24 16:56:05 +02:00
type ImgElementStyle = NonNullable < JSX.IntrinsicElements [ ' img ' ] [ ' style ' ] >
type ImgElementWithDataProp = HTMLImageElement & {
'data-loaded-src' : string | undefined
}
export interface StaticImageData {
src : string
height : number
width : number
blurDataURL? : string
}
interface StaticRequire {
default : StaticImageData
}
type StaticImport = StaticRequire | StaticImageData
2022-09-27 07:41:25 +02:00
type SafeNumber = number | ` ${ number } `
2022-06-24 16:56:05 +02:00
function isStaticRequire (
src : StaticRequire | StaticImageData
) : src is StaticRequire {
return ( src as StaticRequire ) . default !== undefined
}
function isStaticImageData (
src : StaticRequire | StaticImageData
) : src is StaticImageData {
return ( src as StaticImageData ) . src !== undefined
}
function isStaticImport ( src : string | StaticImport ) : src is StaticImport {
return (
typeof src === 'object' &&
( isStaticRequire ( src as StaticImport ) ||
isStaticImageData ( src as StaticImport ) )
)
}
export type ImageProps = Omit <
JSX . IntrinsicElements [ 'img' ] ,
2022-10-14 03:59:22 +02:00
'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading'
2022-06-24 16:56:05 +02:00
> & {
src : string | StaticImport
2022-09-27 07:41:25 +02:00
width? : SafeNumber
height? : SafeNumber
2022-10-14 03:59:22 +02:00
layout? : LayoutValue
2022-06-24 16:56:05 +02:00
loader? : ImageLoader
2022-09-27 07:41:25 +02:00
quality? : SafeNumber
2022-06-24 16:56:05 +02:00
priority? : boolean
loading? : LoadingValue
2022-10-14 03:59:22 +02:00
lazyRoot? : React.RefObject < HTMLElement > | null
lazyBoundary? : string
2022-06-24 16:56:05 +02:00
placeholder? : PlaceholderValue
blurDataURL? : string
unoptimized? : boolean
2022-10-14 03:59:22 +02:00
objectFit? : ImgElementStyle [ 'objectFit' ]
objectPosition? : ImgElementStyle [ 'objectPosition' ]
2022-06-24 16:56:05 +02:00
onLoadingComplete? : OnLoadingComplete
}
2022-10-14 03:59:22 +02:00
type ImageElementProps = Omit < ImageProps , ' src ' | ' loader ' > & {
2022-06-24 16:56:05 +02:00
srcString : string
imgAttributes : GenImgAttrsResult
heightInt : number | undefined
widthInt : number | undefined
qualityInt : number | undefined
2022-10-14 03:59:22 +02:00
layout : LayoutValue
2022-06-24 16:56:05 +02:00
imgStyle : ImgElementStyle
blurStyle : ImgElementStyle
isLazy : boolean
loading : LoadingValue
config : ImageConfig
unoptimized : boolean
loader : ImageLoaderWithConfig
placeholder : PlaceholderValue
onLoadingCompleteRef : React.MutableRefObject < OnLoadingComplete | undefined >
setBlurComplete : ( b : boolean ) = > void
2022-10-14 03:59:22 +02:00
setIntersection : ( img : HTMLImageElement | null ) = > void
isVisible : boolean
noscriptSizes : string | undefined
2022-06-24 16:56:05 +02:00
}
function getWidths (
{ deviceSizes , allSizes } : ImageConfig ,
width : number | undefined ,
2022-10-14 03:59:22 +02:00
layout : LayoutValue ,
2022-06-24 16:56:05 +02:00
sizes : string | undefined
) : { widths : number [ ] ; kind : 'w' | 'x' } {
2022-10-14 03:59:22 +02:00
if ( sizes && ( layout === 'fill' || layout === 'responsive' ) ) {
2022-06-24 16:56:05 +02:00
// Find all the "vw" percent sizes used in the sizes prop
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g
const percentSizes = [ ]
for ( let match ; ( match = viewportWidthRe . exec ( sizes ) ) ; match ) {
percentSizes . push ( parseInt ( match [ 2 ] ) )
}
if ( percentSizes . length ) {
const smallestRatio = Math . min ( . . . percentSizes ) * 0.01
return {
widths : allSizes.filter ( ( s ) = > s >= deviceSizes [ 0 ] * smallestRatio ) ,
kind : 'w' ,
}
}
return { widths : allSizes , kind : 'w' }
}
2022-10-14 03:59:22 +02:00
if (
typeof width !== 'number' ||
layout === 'fill' ||
layout === 'responsive'
) {
2022-06-24 16:56:05 +02:00
return { widths : deviceSizes , kind : 'w' }
}
const widths = [
. . . new Set (
// > This means that most OLED screens that say they are 3x resolution,
// > are actually 3x in the green color, but only 1.5x in the red and
// > blue colors. Showing a 3x resolution image in the app vs a 2x
// > resolution image will be visually the same, though the 3x image
// > takes significantly more data. Even true 3x resolution screens are
// > 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' }
}
type GenImgAttrsData = {
config : ImageConfig
src : string
unoptimized : boolean
2022-10-14 03:59:22 +02:00
layout : LayoutValue
2022-06-24 16:56:05 +02:00
loader : ImageLoaderWithConfig
width? : number
quality? : number
sizes? : string
}
type GenImgAttrsResult = {
src : string
srcSet : string | undefined
sizes : string | undefined
}
function generateImgAttrs ( {
config ,
src ,
unoptimized ,
2022-10-14 03:59:22 +02:00
layout ,
2022-06-24 16:56:05 +02:00
width ,
quality ,
sizes ,
loader ,
} : GenImgAttrsData ) : GenImgAttrsResult {
if ( unoptimized ) {
return { src , srcSet : undefined , sizes : undefined }
}
2022-10-14 03:59:22 +02:00
const { widths , kind } = getWidths ( config , width , layout , sizes )
2022-06-24 16:56:05 +02:00
const last = widths . length - 1
return {
sizes : ! sizes && kind === 'w' ? '100vw' : sizes ,
srcSet : widths
. map (
( w , i ) = >
` ${ loader ( { config , src , quality , width : w } )} ${
kind === 'w' ? w : i + 1
} $ { kind } `
)
. join ( ', ' ) ,
// It's intended to keep `src` the last attribute because React updates
// attributes in order. If we keep `src` the first one, Safari will
// immediately start to fetch `src`, before `sizes` and `srcSet` are even
// updated by React. That causes multiple unnecessary requests if `srcSet`
// and `sizes` are defined.
// This bug cannot be reproduced in Chrome or Firefox.
src : loader ( { config , src , quality , width : widths [ last ] } ) ,
}
}
function getInt ( x : unknown ) : number | undefined {
2022-10-14 03:59:22 +02:00
if ( typeof x === 'number' ) {
2022-06-24 16:56:05 +02:00
return x
}
2022-10-14 03:59:22 +02:00
if ( typeof x === 'string' ) {
2022-06-24 16:56:05 +02:00
return parseInt ( x , 10 )
}
2022-10-14 03:59:22 +02:00
return undefined
}
function defaultImageLoader ( loaderProps : ImageLoaderPropsWithConfig ) {
const loaderKey = loaderProps . config ? . loader || 'default'
const load = loaders . get ( loaderKey )
if ( load ) {
return load ( loaderProps )
}
throw new Error (
` Unknown "loader" found in "next.config.js". Expected: ${ VALID_LOADERS . join (
', '
) } . Received : $ { loaderKey } `
)
2022-06-24 16:56:05 +02:00
}
// See https://stackoverflow.com/q/39777833/266535 for why we use this ref
// handler instead of the img's onLoad attribute.
function handleLoading (
img : ImgElementWithDataProp ,
src : string ,
2022-10-14 03:59:22 +02:00
layout : LayoutValue ,
2022-06-24 16:56:05 +02:00
placeholder : PlaceholderValue ,
onLoadingCompleteRef : React.MutableRefObject < OnLoadingComplete | undefined > ,
setBlurComplete : ( b : boolean ) = > void
) {
2022-10-14 03:59:22 +02:00
if ( ! img || img . src === emptyDataURL || img [ 'data-loaded-src' ] === src ) {
2022-06-24 16:56:05 +02:00
return
}
img [ 'data-loaded-src' ] = src
const p = 'decode' in img ? img . decode ( ) : Promise . resolve ( )
p . catch ( ( ) = > { } ) . then ( ( ) = > {
if ( ! img . parentNode ) {
// Exit early in case of race condition:
// - onload() is called
// - decode() is called but incomplete
// - unmount is called
// - decode() completes
return
}
2022-10-14 03:59:22 +02:00
loadedImageURLs . add ( src )
2022-06-24 16:56:05 +02:00
if ( placeholder === 'blur' ) {
setBlurComplete ( true )
}
if ( onLoadingCompleteRef ? . current ) {
2022-10-14 03:59:22 +02:00
const { naturalWidth , naturalHeight } = img
// Pass back read-only primitive values but not the
// underlying DOM element because it could be misused.
onLoadingCompleteRef . current ( { naturalWidth , naturalHeight } )
2022-06-24 16:56:05 +02:00
}
if ( process . env . NODE_ENV !== 'production' ) {
2022-10-14 03:59:22 +02:00
if ( img . parentElement ? . parentElement ) {
const parent = getComputedStyle ( img . parentElement . parentElement )
if ( ! parent . position ) {
// The parent has not been rendered to the dom yet and therefore it has no position. Skip the warnings for such cases.
} else if ( layout === 'responsive' && parent . display === 'flex' ) {
warnOnce (
` Image with src " ${ src } " may not render properly as a child of a flex container. Consider wrapping the image with a div to configure the width. `
)
} else if (
layout === 'fill' &&
parent . position !== 'relative' &&
parent . position !== 'fixed' &&
parent . position !== 'absolute'
2022-08-01 20:21:36 +02:00
) {
warnOnce (
2022-10-14 03:59:22 +02:00
` Image with src " ${ src } " may not render properly with a parent using position:" ${ parent . position } ". Consider changing the parent style to position:"relative" with a width and height. `
2022-08-01 20:21:36 +02:00
)
}
}
2022-06-24 16:56:05 +02:00
}
} )
}
2022-08-11 23:32:52 +02:00
const ImageElement = ( {
imgAttributes ,
heightInt ,
widthInt ,
qualityInt ,
2022-10-14 03:59:22 +02:00
layout ,
2022-08-11 23:32:52 +02:00
className ,
imgStyle ,
blurStyle ,
isLazy ,
placeholder ,
loading ,
srcString ,
config ,
unoptimized ,
loader ,
onLoadingCompleteRef ,
setBlurComplete ,
2022-10-14 03:59:22 +02:00
setIntersection ,
2022-08-11 23:32:52 +02:00
onLoad ,
onError ,
2022-10-14 03:59:22 +02:00
isVisible ,
noscriptSizes ,
2022-08-11 23:32:52 +02:00
. . . rest
} : ImageElementProps ) = > {
loading = isLazy ? 'lazy' : loading
return (
< >
< img
{ . . . rest }
{ . . . imgAttributes }
decoding = "async"
2022-10-14 03:59:22 +02:00
data - nimg = { layout }
2022-08-11 23:32:52 +02:00
className = { className }
style = { { . . . imgStyle , . . . blurStyle } }
ref = { useCallback (
2022-10-14 03:59:22 +02:00
( img : ImgElementWithDataProp ) = > {
2022-08-11 23:32:52 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2022-10-14 03:59:22 +02:00
if ( img && ! srcString ) {
2022-08-11 23:32:52 +02:00
console . error ( ` Image is missing required "src" property: ` , img )
}
}
2022-10-14 03:59:22 +02:00
setIntersection ( img )
if ( img ? . complete ) {
2022-08-11 23:32:52 +02:00
handleLoading (
img ,
srcString ,
2022-10-14 03:59:22 +02:00
layout ,
2022-08-11 23:32:52 +02:00
placeholder ,
onLoadingCompleteRef ,
setBlurComplete
)
}
} ,
2022-08-22 23:37:16 +02:00
[
2022-10-14 03:59:22 +02:00
setIntersection ,
2022-08-22 23:37:16 +02:00
srcString ,
2022-10-14 03:59:22 +02:00
layout ,
2022-08-22 23:37:16 +02:00
placeholder ,
onLoadingCompleteRef ,
setBlurComplete ,
]
2022-08-11 23:32:52 +02:00
) }
onLoad = { ( event ) = > {
const img = event . currentTarget as ImgElementWithDataProp
handleLoading (
img ,
srcString ,
2022-10-14 03:59:22 +02:00
layout ,
2022-08-11 23:32:52 +02:00
placeholder ,
onLoadingCompleteRef ,
setBlurComplete
)
2022-10-14 03:59:22 +02:00
if ( onLoad ) {
onLoad ( event )
}
2022-08-11 23:32:52 +02:00
} }
onError = { ( event ) = > {
if ( placeholder === 'blur' ) {
// If the real image fails to load, this will still remove the placeholder.
setBlurComplete ( true )
}
if ( onError ) {
onError ( event )
}
} }
/ >
2022-10-14 03:59:22 +02:00
{ ( isLazy || placeholder === 'blur' ) && (
< noscript >
< img
{ . . . rest }
{ . . . generateImgAttrs ( {
config ,
src : srcString ,
unoptimized ,
layout ,
width : widthInt ,
quality : qualityInt ,
sizes : noscriptSizes ,
loader ,
} ) }
decoding = "async"
data - nimg = { layout }
style = { imgStyle }
className = { className }
// @ts-ignore - TODO: upgrade to `@types/react@17`
loading = { loading }
/ >
< / noscript >
) }
2022-08-11 23:32:52 +02:00
< / >
)
}
2022-06-24 16:56:05 +02:00
export default function Image ( {
src ,
sizes ,
unoptimized = false ,
priority = false ,
loading ,
2022-10-14 03:59:22 +02:00
lazyRoot = null ,
lazyBoundary ,
2022-06-24 16:56:05 +02:00
className ,
quality ,
width ,
height ,
style ,
2022-10-14 03:59:22 +02:00
objectFit ,
objectPosition ,
2022-06-24 16:56:05 +02:00
onLoadingComplete ,
placeholder = 'empty' ,
blurDataURL ,
. . . all
} : ImageProps ) {
const configContext = useContext ( ImageConfigContext )
const config : ImageConfig = useMemo ( ( ) = > {
const c = configEnv || configContext || imageConfigDefault
const allSizes = [ . . . c . deviceSizes , . . . c . imageSizes ] . sort ( ( a , b ) = > a - b )
const deviceSizes = c . deviceSizes . sort ( ( a , b ) = > a - b )
return { . . . c , allSizes , deviceSizes }
} , [ configContext ] )
let rest : Partial < ImageProps > = all
2022-10-14 03:59:22 +02:00
let layout : NonNullable < LayoutValue > = sizes ? 'responsive' : 'intrinsic'
if ( 'layout' in rest ) {
// Override default layout if the user specified one:
if ( rest . layout ) layout = rest . layout
2022-06-24 16:56:05 +02:00
2022-10-14 03:59:22 +02:00
// Remove property so it's not spread on <img>:
delete rest . layout
}
let loader : ImageLoaderWithConfig = defaultImageLoader
2022-06-24 16:56:05 +02:00
if ( 'loader' in rest ) {
if ( rest . loader ) {
const customImageLoader = rest . loader
loader = ( obj ) = > {
const { config : _ , . . . opts } = obj
// The config object is internal only so we must
// not pass it to the user-defined loader()
return customImageLoader ( opts )
}
}
// Remove property so it's not spread on <img>
delete rest . loader
}
let staticSrc = ''
if ( isStaticImport ( src ) ) {
const staticImageData = isStaticRequire ( src ) ? src.default : src
if ( ! staticImageData . src ) {
throw new Error (
` An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${ JSON . stringify (
staticImageData
) } `
)
}
2022-09-06 19:50:31 +02:00
blurDataURL = blurDataURL || staticImageData . blurDataURL
staticSrc = staticImageData . src
2022-10-14 03:59:22 +02:00
if ( ! layout || layout !== 'fill' ) {
height = height || staticImageData . height
width = width || staticImageData . width
if ( ! staticImageData . height || ! staticImageData . width ) {
throw new Error (
` An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${ JSON . stringify (
staticImageData
) } `
)
2022-09-06 19:50:31 +02:00
}
}
2022-06-24 16:56:05 +02:00
}
src = typeof src === 'string' ? src : staticSrc
let isLazy =
! priority && ( loading === 'lazy' || typeof loading === 'undefined' )
if ( src . startsWith ( 'data:' ) || src . startsWith ( 'blob:' ) ) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
unoptimized = true
isLazy = false
}
2022-10-14 03:59:22 +02:00
if ( typeof window !== 'undefined' && loadedImageURLs . has ( src ) ) {
isLazy = false
}
2022-09-01 00:44:17 +02:00
if ( config . unoptimized ) {
2022-06-24 16:56:05 +02:00
unoptimized = true
}
const [ blurComplete , setBlurComplete ] = useState ( false )
2022-10-14 03:59:22 +02:00
const [ setIntersection , isIntersected , resetIntersected ] =
useIntersection < HTMLImageElement > ( {
rootRef : lazyRoot ,
rootMargin : lazyBoundary || '200px' ,
disabled : ! isLazy ,
} )
const isVisible = ! isLazy || isIntersected
const wrapperStyle : JSX.IntrinsicElements [ 'span' ] [ 'style' ] = {
boxSizing : 'border-box' ,
display : 'block' ,
overflow : 'hidden' ,
width : 'initial' ,
height : 'initial' ,
background : 'none' ,
opacity : 1 ,
border : 0 ,
margin : 0 ,
padding : 0 ,
}
const sizerStyle : JSX.IntrinsicElements [ 'span' ] [ 'style' ] = {
boxSizing : 'border-box' ,
display : 'block' ,
width : 'initial' ,
height : 'initial' ,
background : 'none' ,
opacity : 1 ,
border : 0 ,
margin : 0 ,
padding : 0 ,
}
let hasSizer = false
let sizerSvgUrl : string | undefined
const layoutStyle : ImgElementStyle = {
position : 'absolute' ,
top : 0 ,
left : 0 ,
bottom : 0 ,
right : 0 ,
boxSizing : 'border-box' ,
padding : 0 ,
border : 'none' ,
margin : 'auto' ,
display : 'block' ,
width : 0 ,
height : 0 ,
minWidth : '100%' ,
maxWidth : '100%' ,
minHeight : '100%' ,
maxHeight : '100%' ,
objectFit ,
objectPosition ,
}
2022-09-06 19:50:31 +02:00
2022-10-14 03:59:22 +02:00
let widthInt = getInt ( width )
let heightInt = getInt ( height )
2022-07-20 23:26:38 +02:00
const qualityInt = getInt ( quality )
2022-06-24 16:56:05 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
if ( ! src ) {
2022-07-20 23:26:38 +02:00
// React doesn't show the stack trace and there's
// no `src` to help identify which image, so we
// instead console.error(ref) during mount.
2022-10-14 03:59:22 +02:00
widthInt = widthInt || 1
heightInt = heightInt || 1
2022-07-20 23:26:38 +02:00
unoptimized = true
} else {
2022-10-14 03:59:22 +02:00
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 ( ',' ) } . `
)
2022-07-20 23:26:38 +02:00
}
2022-08-01 20:21:36 +02:00
2022-10-14 03:59:22 +02:00
if (
( typeof widthInt !== 'undefined' && isNaN ( widthInt ) ) ||
( typeof heightInt !== 'undefined' && isNaN ( heightInt ) )
) {
throw new Error (
` Image with src " ${ src } " has invalid "width" or "height" property. These should be numeric values. `
)
}
if ( layout === 'fill' && ( width || height ) ) {
2022-06-24 16:56:05 +02:00
warnOnce (
2022-10-14 03:59:22 +02:00
` Image with src " ${ src } " and "layout='fill'" has unused properties assigned. Please remove "width" and "height". `
2022-06-24 16:56:05 +02:00
)
}
2022-10-14 03:59:22 +02:00
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' ) {
2022-08-01 20:21:36 +02:00
throw new Error (
2022-10-14 03:59:22 +02:00
` Image with src " ${ src } " has both "priority" and "loading='lazy'" properties. Only one should be used. `
)
}
if ( sizes && layout !== 'fill' && layout !== 'responsive' ) {
warnOnce (
` Image with src " ${ src } " has "sizes" property but it will be ignored. Only use "sizes" with "layout='fill'" or "layout='responsive'" `
)
}
if ( placeholder === 'blur' ) {
if ( layout !== 'fill' && ( widthInt || 0 ) * ( heightInt || 0 ) < 1600 ) {
warnOnce (
` Image with src " ${ src } " is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance. `
)
}
if ( ! blurDataURL ) {
const VALID_BLUR_EXT = [ 'jpeg' , 'png' , 'webp' , 'avif' ] // should match next-image-loader
throw new Error (
` Image with src " ${ src } " has "placeholder='blur'" property but is missing the "blurDataURL" property.
2022-08-01 20:21:36 +02:00
Possible solutions :
- Add a "blurDataURL" property , the contents should be a small Data URL to represent the image
- Change the "src" property to a static import with one of the supported file types : $ { VALID_BLUR_EXT . join (
','
) }
- Remove the "placeholder" property , effectively no blur effect
Read more : https : //nextjs.org/docs/messages/placeholder-blur-data-url`
2022-10-14 03:59:22 +02:00
)
}
}
if ( 'ref' in rest ) {
warnOnce (
` Image with src " ${ src } " is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead. `
2022-08-01 20:21:36 +02:00
)
2022-07-20 23:26:38 +02:00
}
2022-10-14 03:59:22 +02:00
if ( ! unoptimized && loader !== defaultImageLoader ) {
const urlStr = loader ( {
config ,
src ,
width : widthInt || 400 ,
quality : qualityInt || 75 ,
} )
let url : URL | undefined
try {
url = new URL ( urlStr )
} catch ( err ) { }
if ( urlStr === src || ( url && url . pathname === src && ! url . search ) ) {
warnOnce (
` Image with src " ${ src } " has a "loader" property that does not implement width. Please implement it or use the "unoptimized" property instead. ` +
` \ nRead more: https://nextjs.org/docs/messages/next-image-missing-loader-width `
)
}
}
if ( style ) {
let overwrittenStyles = Object . keys ( style ) . filter (
( key ) = > key in layoutStyle
2022-08-01 20:21:36 +02:00
)
2022-10-14 03:59:22 +02:00
if ( overwrittenStyles . length ) {
warnOnce (
` Image with src ${ src } is assigned the following styles, which are overwritten by automatically-generated styles: ${ overwrittenStyles . join (
', '
) } `
)
}
2022-08-01 20:21:36 +02:00
}
2022-10-14 03:59:22 +02:00
if (
typeof window !== 'undefined' &&
! perfObserver &&
window . PerformanceObserver
) {
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
warnOnce (
` 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. ` +
2022-10-17 16:41:35 +02:00
` \ nRead more: https://nextjs.org/docs/api-reference/next/legacy/image#priority `
2022-10-14 03:59:22 +02:00
)
}
2022-07-20 23:26:38 +02:00
}
2022-08-01 20:21:36 +02:00
} )
2022-10-14 03:59:22 +02:00
try {
perfObserver . observe ( {
type : 'largest-contentful-paint' ,
buffered : true ,
} )
} catch ( err ) {
// Log error but don't crash the app
console . error ( err )
}
2022-06-24 16:56:05 +02:00
}
}
}
2022-10-14 03:59:22 +02:00
const imgStyle = Object . assign ( { } , style , layoutStyle )
2022-06-24 16:56:05 +02:00
const blurStyle =
2022-10-14 03:59:22 +02:00
placeholder === 'blur' && ! blurComplete
2022-06-24 16:56:05 +02:00
? {
2022-10-14 03:59:22 +02:00
backgroundSize : objectFit || 'cover' ,
backgroundPosition : objectPosition || '0% 0%' ,
filter : 'blur(20px)' ,
backgroundImage : ` url(" ${ blurDataURL } ") ` ,
2022-06-24 16:56:05 +02:00
}
: { }
2022-10-14 03:59:22 +02:00
if ( layout === 'fill' ) {
// <Image src="i.png" layout="fill" />
wrapperStyle . display = 'block'
wrapperStyle . position = 'absolute'
wrapperStyle . top = 0
wrapperStyle . left = 0
wrapperStyle . bottom = 0
wrapperStyle . right = 0
} else if (
typeof widthInt !== 'undefined' &&
typeof heightInt !== 'undefined'
) {
// <Image src="i.png" width="100" height="100" />
const quotient = heightInt / widthInt
const paddingTop = isNaN ( quotient ) ? '100%' : ` ${ quotient * 100 } % `
if ( layout === 'responsive' ) {
// <Image src="i.png" width="100" height="100" layout="responsive" />
wrapperStyle . display = 'block'
wrapperStyle . position = 'relative'
hasSizer = true
sizerStyle . paddingTop = paddingTop
} else if ( layout === 'intrinsic' ) {
// <Image src="i.png" width="100" height="100" layout="intrinsic" />
wrapperStyle . display = 'inline-block'
wrapperStyle . position = 'relative'
wrapperStyle . maxWidth = '100%'
hasSizer = true
sizerStyle . maxWidth = '100%'
sizerSvgUrl = ` data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20version=%271.1%27%20width=%27 ${ widthInt } %27%20height=%27 ${ heightInt } %27/%3e `
} else if ( layout === 'fixed' ) {
// <Image src="i.png" width="100" height="100" layout="fixed" />
wrapperStyle . display = 'inline-block'
wrapperStyle . position = 'relative'
wrapperStyle . width = widthInt
wrapperStyle . height = heightInt
}
} 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. `
)
2022-08-30 00:19:39 +02:00
}
}
2022-10-14 03:59:22 +02:00
let imgAttributes : GenImgAttrsResult = {
src : emptyDataURL ,
srcSet : undefined ,
sizes : undefined ,
}
if ( isVisible ) {
imgAttributes = generateImgAttrs ( {
config ,
src ,
unoptimized ,
layout ,
width : widthInt ,
quality : qualityInt ,
sizes ,
loader ,
} )
}
2022-06-24 16:56:05 +02:00
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 } )
}
}
2022-09-19 19:25:27 +02:00
const linkProps : React.DetailedHTMLProps <
React . LinkHTMLAttributes < HTMLLinkElement > ,
HTMLLinkElement
> = {
2022-10-29 22:34:03 +02:00
// @ts-expect-error upgrade react types to react 18
imageSrcSet : imgAttributes.srcSet ,
imageSizes : imgAttributes.sizes ,
2022-09-19 19:25:27 +02:00
crossOrigin : rest.crossOrigin ,
2022-06-24 16:56:05 +02:00
}
2022-10-14 03:59:22 +02:00
const useLayoutEffect =
typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect
2022-06-24 16:56:05 +02:00
const onLoadingCompleteRef = useRef ( onLoadingComplete )
2022-10-14 03:59:22 +02:00
const previousImageSrc = useRef < string | StaticImport > ( src )
2022-06-24 16:56:05 +02:00
useEffect ( ( ) = > {
onLoadingCompleteRef . current = onLoadingComplete
} , [ onLoadingComplete ] )
2022-10-14 03:59:22 +02:00
useLayoutEffect ( ( ) = > {
if ( previousImageSrc . current !== src ) {
resetIntersected ( )
previousImageSrc . current = src
}
} , [ resetIntersected , src ] )
const imgElementArgs = {
2022-06-24 16:56:05 +02:00
isLazy ,
imgAttributes ,
heightInt ,
widthInt ,
qualityInt ,
2022-10-14 03:59:22 +02:00
layout ,
2022-06-24 16:56:05 +02:00
className ,
imgStyle ,
blurStyle ,
loading ,
config ,
unoptimized ,
placeholder ,
loader ,
srcString ,
onLoadingCompleteRef ,
setBlurComplete ,
2022-10-14 03:59:22 +02:00
setIntersection ,
isVisible ,
noscriptSizes : sizes ,
2022-06-24 16:56:05 +02:00
. . . rest ,
}
return (
< >
2022-10-14 03:59:22 +02:00
{
< span style = { wrapperStyle } >
{ hasSizer ? (
< span style = { sizerStyle } >
{ sizerSvgUrl ? (
< img
style = { {
display : 'block' ,
maxWidth : '100%' ,
width : 'initial' ,
height : 'initial' ,
background : 'none' ,
opacity : 1 ,
border : 0 ,
margin : 0 ,
padding : 0 ,
} }
alt = ""
aria - hidden = { true }
src = { sizerSvgUrl }
/ >
) : null }
< / span >
) : null }
< ImageElement { ...imgElementArgs } / >
< / span >
}
2022-06-24 16:56:05 +02:00
{ priority ? (
// 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.
//
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset
< Head >
< link
key = {
'__nimg-' +
imgAttributes . src +
imgAttributes . srcSet +
imgAttributes . sizes
}
rel = "preload"
as = "image"
href = { imgAttributes . srcSet ? undefined : imgAttributes . src }
{ . . . linkProps }
/ >
< / Head >
) : null }
< / >
)
}