2022-10-11 19:26:45 +02:00
'use client'
2022-09-17 00:12:59 +02:00
2022-04-06 00:47:55 +02:00
import React , {
useRef ,
useEffect ,
useCallback ,
useContext ,
useMemo ,
useState ,
} from 'react'
2021-06-30 11:43:31 +02:00
import Head from '../shared/lib/head'
2022-10-14 03:59:22 +02:00
import { getImageBlurSvg } from '../shared/lib/image-blur-svg'
2020-11-12 20:24:08 +01:00
import {
2021-09-09 00:03:52 +02:00
ImageConfigComplete ,
2020-11-12 20:24:08 +01:00
imageConfigDefault ,
2022-02-22 15:27:18 +01:00
} from '../shared/lib/image-config'
2022-02-09 01:55:53 +01:00
import { ImageConfigContext } from '../shared/lib/image-config-context'
2022-02-28 23:39:51 +01:00
import { warnOnce } from '../shared/lib/utils'
2022-08-15 16:29:51 +02:00
2022-02-09 01:55:53 +01:00
const configEnv = process . env . __NEXT_IMAGE_OPTS as any as ImageConfigComplete
2021-10-25 23:59:00 +02:00
const allImgs = new Map <
string ,
{ src : string ; priority : boolean ; placeholder : string }
> ( )
let perfObserver : PerformanceObserver | undefined
2021-07-07 19:51:16 +02:00
2020-11-11 16:46:48 +01:00
if ( typeof window === 'undefined' ) {
; ( global as any ) . __NEXT_IMAGE_IMPORTED = true
}
2020-10-22 20:59:42 +02:00
const VALID_LOADING_VALUES = [ 'lazy' , 'eager' , undefined ] as const
type LoadingValue = typeof VALID_LOADING_VALUES [ number ]
2022-02-09 01:55:53 +01:00
type ImageConfig = ImageConfigComplete & { allSizes : number [ ] }
2022-10-14 03:59:22 +02:00
export type ImageLoader = ( p : ImageLoaderProps ) = > string
2021-01-05 21:51:34 +01:00
export type ImageLoaderProps = {
src : string
width : number
quality? : number
}
2022-04-09 00:19:25 +02:00
// 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 = ( p : ImageLoaderPropsWithConfig ) = > string
2022-04-09 00:19:25 +02:00
type ImageLoaderPropsWithConfig = ImageLoaderProps & {
config : Readonly < ImageConfig >
}
2021-04-30 19:05:03 +02:00
type PlaceholderValue = 'blur' | 'empty'
2022-10-14 03:59:22 +02:00
type OnLoad = React . ReactEventHandler < HTMLImageElement > | undefined
type OnLoadingComplete = ( img : HTMLImageElement ) = > void
2021-08-03 01:14:38 +02:00
2020-11-05 20:42:55 +01:00
type ImgElementStyle = NonNullable < JSX.IntrinsicElements [ ' img ' ] [ ' style ' ] >
2022-04-06 00:47:55 +02:00
type ImgElementWithDataProp = HTMLImageElement & {
'data-loaded-src' : string | undefined
}
2022-02-19 04:25:49 +01:00
export interface StaticImageData {
src : string
height : number
width : number
blurDataURL? : string
2022-10-14 03:59:22 +02:00
blurWidth? : number
blurHeight? : number
2022-02-19 04:25:49 +01:00
}
2021-06-04 10:06:00 +02:00
interface StaticRequire {
default : StaticImageData
}
type StaticImport = StaticRequire | StaticImageData
2022-09-27 07:41:25 +02:00
type SafeNumber = number | ` ${ number } `
2021-06-04 10:06:00 +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 ) )
)
}
2020-11-09 07:20:54 +01:00
export type ImageProps = Omit <
2020-10-18 12:41:25 +02:00
JSX . IntrinsicElements [ 'img' ] ,
2022-10-14 03:59:22 +02:00
'src' | 'srcSet' | 'ref' | 'alt' | 'width' | 'height' | 'loading'
2020-10-18 12:41:25 +02:00
> & {
2021-07-07 23:31:30 +02:00
src : string | StaticImport
2022-10-14 03:59:22 +02:00
alt : string
2022-09-27 07:41:25 +02:00
width? : SafeNumber
height? : SafeNumber
2022-10-14 03:59:22 +02:00
fill? : boolean
2021-01-05 21:51:34 +01:00
loader? : ImageLoader
2022-09-27 07:41:25 +02:00
quality? : SafeNumber
2020-10-16 22:04:39 +02:00
priority? : boolean
2020-10-22 20:59:42 +02:00
loading? : LoadingValue
2021-07-07 23:31:30 +02:00
placeholder? : PlaceholderValue
blurDataURL? : string
2020-10-16 22:04:39 +02:00
unoptimized? : boolean
2021-08-03 01:14:38 +02:00
onLoadingComplete? : OnLoadingComplete
2021-07-07 23:31:30 +02:00
}
2020-10-14 11:57:10 +02:00
2022-10-14 03:59:22 +02:00
type ImageElementProps = Omit < ImageProps , ' src ' | ' alt ' | ' loader ' > & {
2022-03-14 15:25:23 +01:00
srcString : string
imgAttributes : GenImgAttrsResult
heightInt : number | undefined
widthInt : number | undefined
qualityInt : number | undefined
imgStyle : ImgElementStyle
blurStyle : ImgElementStyle
isLazy : boolean
2022-10-14 03:59:22 +02:00
fill? : boolean
2022-03-14 15:25:23 +01:00
loading : LoadingValue
config : ImageConfig
unoptimized : boolean
2022-04-09 00:19:25 +02:00
loader : ImageLoaderWithConfig
2022-04-06 00:47:55 +02:00
placeholder : PlaceholderValue
2022-10-14 03:59:22 +02:00
onLoadRef : React.MutableRefObject < OnLoad | undefined >
2022-04-06 00:47:55 +02:00
onLoadingCompleteRef : React.MutableRefObject < OnLoadingComplete | undefined >
setBlurComplete : ( b : boolean ) = > void
2022-10-14 03:59:22 +02:00
setShowAltText : ( b : boolean ) = > void
2022-03-14 15:25:23 +01:00
}
2020-11-13 05:30:41 +01:00
function getWidths (
2022-02-09 01:55:53 +01:00
{ deviceSizes , allSizes } : ImageConfig ,
2020-11-01 01:26:57 +01:00
width : number | undefined ,
2021-02-24 23:57:19 +01:00
sizes : string | undefined
2020-11-13 05:30:41 +01:00
) : { widths : number [ ] ; kind : 'w' | 'x' } {
2022-10-14 03:59:22 +02:00
if ( sizes ) {
2021-02-24 23:57:19 +01:00
// Find all the "vw" percent sizes used in the sizes prop
2021-04-29 12:07:27 +02:00
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g
const percentSizes = [ ]
for ( let match ; ( match = viewportWidthRe . exec ( sizes ) ) ; match ) {
percentSizes . push ( parseInt ( match [ 2 ] ) )
}
2021-02-24 23:57:19 +01:00
if ( percentSizes . length ) {
const smallestRatio = Math . min ( . . . percentSizes ) * 0.01
return {
2022-02-09 01:55:53 +01:00
widths : allSizes.filter ( ( s ) = > s >= deviceSizes [ 0 ] * smallestRatio ) ,
2021-02-24 23:57:19 +01:00
kind : 'w' ,
}
}
return { widths : allSizes , kind : 'w' }
}
2022-10-14 03:59:22 +02:00
if ( typeof width !== 'number' ) {
2022-02-09 01:55:53 +01:00
return { widths : deviceSizes , kind : 'w' }
2020-10-26 21:07:52 +01:00
}
2020-11-03 03:12:46 +01:00
2020-11-13 05:30:41 +01:00
const widths = [
2020-11-03 03:12:46 +01:00
. . . new Set (
2020-12-30 19:17:46 +01:00
// > 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 (
2020-11-03 03:12:46 +01:00
( w ) = > allSizes . find ( ( p ) = > p >= w ) || allSizes [ allSizes . length - 1 ]
)
) ,
]
2020-11-13 05:30:41 +01:00
return { widths , kind : 'x' }
2020-10-14 11:57:10 +02:00
}
2020-11-13 05:30:41 +01:00
type GenImgAttrsData = {
2022-02-09 01:55:53 +01:00
config : ImageConfig
2020-10-14 11:57:10 +02:00
src : string
2020-10-26 15:29:52 +01:00
unoptimized : boolean
2022-04-09 00:19:25 +02:00
loader : ImageLoaderWithConfig
2020-10-27 12:37:55 +01:00
width? : number
quality? : number
2020-11-13 05:30:41 +01:00
sizes? : string
2020-10-14 11:57:10 +02:00
}
2020-12-31 22:08:57 +01:00
type GenImgAttrsResult = {
src : string
srcSet : string | undefined
sizes : string | undefined
}
2020-11-13 05:30:41 +01:00
function generateImgAttrs ( {
2022-02-09 01:55:53 +01:00
config ,
2020-10-26 15:29:52 +01:00
src ,
unoptimized ,
width ,
quality ,
2020-11-13 05:30:41 +01:00
sizes ,
2021-01-05 21:51:34 +01:00
loader ,
2020-11-13 05:30:41 +01:00
} : GenImgAttrsData ) : GenImgAttrsResult {
2020-10-26 15:29:52 +01:00
if ( unoptimized ) {
2020-12-31 22:08:57 +01:00
return { src , srcSet : undefined , sizes : undefined }
2020-10-26 15:29:52 +01:00
}
2020-10-26 21:07:52 +01:00
2022-10-14 03:59:22 +02:00
const { widths , kind } = getWidths ( config , width , sizes )
2020-11-13 05:30:41 +01:00
const last = widths . length - 1
2021-01-05 21:51:34 +01:00
return {
sizes : ! sizes && kind === 'w' ? '100vw' : sizes ,
srcSet : widths
. map (
( w , i ) = >
2022-02-09 01:55:53 +01:00
` ${ loader ( { config , src , quality , width : w } )} ${
2021-01-05 21:51:34 +01:00
kind === 'w' ? w : i + 1
} $ { kind } `
)
. join ( ', ' ) ,
2021-03-09 20:07:01 +01:00
// 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.
2022-02-09 01:55:53 +01:00
src : loader ( { config , src , quality , width : widths [ last ] } ) ,
2020-11-13 05:30:41 +01:00
}
2020-10-14 11:57:10 +02:00
}
2020-10-26 15:29:52 +01:00
function getInt ( x : unknown ) : number | undefined {
2022-10-14 03:59:22 +02:00
if ( typeof x === 'number' || typeof x === 'undefined' ) {
2020-10-26 15:29:52 +01:00
return x
}
2022-10-14 03:59:22 +02:00
if ( typeof x === 'string' && /^[0-9]+$/ . test ( x ) ) {
2020-10-26 15:29:52 +01:00
return parseInt ( x , 10 )
}
2022-10-14 03:59:22 +02:00
return NaN
2021-01-05 21:51:34 +01:00
}
2021-04-30 19:05:03 +02:00
// See https://stackoverflow.com/q/39777833/266535 for why we use this ref
// handler instead of the img's onLoad attribute.
2021-07-01 20:51:20 +02:00
function handleLoading (
2022-04-06 00:47:55 +02:00
img : ImgElementWithDataProp ,
2021-07-07 19:51:16 +02:00
src : string ,
2021-07-01 20:51:20 +02:00
placeholder : PlaceholderValue ,
2022-10-14 03:59:22 +02:00
onLoadRef : React.MutableRefObject < OnLoad | undefined > ,
2022-04-06 00:47:55 +02:00
onLoadingCompleteRef : React.MutableRefObject < OnLoadingComplete | undefined > ,
setBlurComplete : ( b : boolean ) = > void
2021-04-30 19:05:03 +02:00
) {
2022-10-14 03:59:22 +02:00
if ( ! img || img [ 'data-loaded-src' ] === src ) {
2022-04-06 00:47:55 +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
2022-01-20 16:29:34 +01:00
return
}
2022-04-06 00:47:55 +02:00
if ( placeholder === 'blur' ) {
setBlurComplete ( true )
2021-04-30 19:05:03 +02:00
}
2022-10-14 03:59:22 +02:00
if ( onLoadRef ? . current ) {
const event = new Event ( 'load' )
Object . defineProperty ( event , 'target' , { writable : false , value : img } )
let prevented = false
let stopped = false
const sytheticEvent = {
. . . event ,
nativeEvent : event ,
currentTarget : img ,
target : img ,
isDefaultPrevented : ( ) = > prevented ,
isPropagationStopped : ( ) = > stopped ,
persist : ( ) = > { } ,
preventDefault : ( ) = > {
prevented = true
event . preventDefault ( )
} ,
stopPropagation : ( ) = > {
stopped = true
event . stopPropagation ( )
} ,
}
onLoadRef . current ( sytheticEvent )
}
2022-04-06 00:47:55 +02:00
if ( onLoadingCompleteRef ? . current ) {
2022-10-14 03:59:22 +02:00
onLoadingCompleteRef . current ( img )
2022-01-20 16:29:34 +01:00
}
2022-04-06 00:47:55 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2022-10-14 03:59:22 +02:00
if ( img . getAttribute ( 'data-nimg' ) === 'future-fill' ) {
if (
! img . getAttribute ( 'sizes' ) ||
img . getAttribute ( 'sizes' ) === '100vw'
2022-04-06 00:47:55 +02:00
) {
2022-10-14 03:59:22 +02:00
let widthViewportRatio =
img . getBoundingClientRect ( ) . width / window . innerWidth
if ( widthViewportRatio < 0.6 ) {
warnOnce (
` Image with src " ${ src } " has "fill" but is missing "sizes" prop. Please add it to improve page performance. Read more: https://nextjs.org/docs/api-reference/next/future/image#sizes `
)
}
}
if ( img . parentElement ) {
const { position } = window . getComputedStyle ( img . parentElement )
const valid = [ 'absolute' , 'fixed' , 'relative' ]
if ( ! valid . includes ( position ) ) {
warnOnce (
` Image with src " ${ src } " has "fill" and parent element with invalid "position". Provided " ${ position } " should be one of ${ valid
. map ( String )
. join ( ',' ) } . `
)
}
}
if ( img . height === 0 ) {
2022-04-06 00:47:55 +02:00
warnOnce (
2022-10-14 03:59:22 +02:00
` Image with src " ${ src } " has "fill" and a height value of 0. This is likely because the parent element of the image has not been styled to have a set height. `
2022-04-06 00:47:55 +02:00
)
}
}
2022-10-14 03:59:22 +02:00
const heightModified =
img . height . toString ( ) !== img . getAttribute ( 'height' )
const widthModified = img . width . toString ( ) !== img . getAttribute ( 'width' )
if (
( heightModified && ! widthModified ) ||
( ! heightModified && widthModified )
) {
warnOnce (
` Image with src " ${ src } " has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. `
)
}
2022-04-06 00:47:55 +02:00
}
} )
2021-04-30 19:05:03 +02:00
}
2022-08-11 23:32:52 +02:00
const ImageElement = ( {
imgAttributes ,
heightInt ,
widthInt ,
qualityInt ,
className ,
imgStyle ,
blurStyle ,
isLazy ,
2022-10-14 03:59:22 +02:00
fill ,
2022-08-11 23:32:52 +02:00
placeholder ,
loading ,
srcString ,
config ,
unoptimized ,
loader ,
2022-10-14 03:59:22 +02:00
onLoadRef ,
2022-08-11 23:32:52 +02:00
onLoadingCompleteRef ,
setBlurComplete ,
2022-10-14 03:59:22 +02:00
setShowAltText ,
2022-08-11 23:32:52 +02:00
onLoad ,
onError ,
. . . rest
} : ImageElementProps ) = > {
loading = isLazy ? 'lazy' : loading
return (
< >
< img
{ . . . rest }
{ . . . imgAttributes }
2022-10-14 03:59:22 +02:00
width = { widthInt }
height = { heightInt }
2022-08-11 23:32:52 +02:00
decoding = "async"
2022-10-14 03:59:22 +02:00
data - nimg = { ` future ${ fill ? '-fill' : '' } ` }
2022-08-11 23:32:52 +02:00
className = { className }
2022-10-14 03:59:22 +02:00
// @ts-ignore - TODO: upgrade to `@types/react@17`
loading = { loading }
2022-08-11 23:32:52 +02:00
style = { { . . . imgStyle , . . . blurStyle } }
ref = { useCallback (
2022-10-14 03:59:22 +02:00
( img : ImgElementWithDataProp | null ) = > {
if ( ! img ) {
return
}
if ( onError ) {
// If the image has an error before react hydrates, then the error is lost.
// The workaround is to wait until the image is mounted which is after hydration,
// then we set the src again to trigger the error handler (if there was an error).
// eslint-disable-next-line no-self-assign
img . src = img . src
}
2022-08-11 23:32:52 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2022-10-14 03:59:22 +02:00
if ( ! 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
if (
img . getAttribute ( 'objectFit' ) ||
img . getAttribute ( 'objectfit' )
) {
console . error (
` Image has unknown prop "objectFit". Did you mean to use the "style" prop instead? ` ,
img
)
}
if (
img . getAttribute ( 'objectPosition' ) ||
img . getAttribute ( 'objectposition' )
) {
console . error (
` Image has unknown prop "objectPosition". Did you mean to use the "style" prop instead? ` ,
img
)
}
if ( img . getAttribute ( 'alt' ) === null ) {
console . error (
` Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines. `
)
}
2022-08-11 23:32:52 +02:00
}
2022-10-14 03:59:22 +02:00
if ( img . complete ) {
2022-08-11 23:32:52 +02:00
handleLoading (
img ,
srcString ,
placeholder ,
2022-10-14 03:59:22 +02:00
onLoadRef ,
2022-08-11 23:32:52 +02:00
onLoadingCompleteRef ,
setBlurComplete
)
}
} ,
[
srcString ,
placeholder ,
2022-10-14 03:59:22 +02:00
onLoadRef ,
2022-08-11 23:32:52 +02:00
onLoadingCompleteRef ,
setBlurComplete ,
2022-10-14 03:59:22 +02:00
onError ,
2022-08-11 23:32:52 +02:00
]
) }
onLoad = { ( event ) = > {
const img = event . currentTarget as ImgElementWithDataProp
handleLoading (
img ,
srcString ,
placeholder ,
2022-10-14 03:59:22 +02:00
onLoadRef ,
2022-08-11 23:32:52 +02:00
onLoadingCompleteRef ,
setBlurComplete
)
} }
onError = { ( event ) = > {
2022-10-14 03:59:22 +02:00
// if the real image fails to load, this will ensure "alt" is visible
setShowAltText ( true )
2022-08-11 23:32:52 +02:00
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
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 ` ${ config . path } ?url= ${ encodeURIComponent ( src ) } &w= ${ width } &q= ${
quality || 75
} `
}
2020-10-14 11:57:10 +02:00
export default function Image ( {
src ,
sizes ,
2020-10-16 22:04:39 +02:00
unoptimized = false ,
priority = false ,
2020-10-22 20:59:42 +02:00
loading ,
2020-10-17 20:55:29 +02:00
className ,
2020-10-20 16:28:01 +02:00
quality ,
2020-10-21 12:03:31 +02:00
width ,
height ,
2022-10-14 03:59:22 +02:00
fill ,
2022-03-14 15:25:23 +01:00
style ,
2022-10-14 03:59:22 +02:00
onLoad ,
2021-07-01 20:51:20 +02:00
onLoadingComplete ,
2021-04-30 19:05:03 +02:00
placeholder = 'empty' ,
blurDataURL ,
2020-10-31 23:34:06 +01:00
. . . all
2020-10-14 11:57:10 +02:00
} : ImageProps ) {
2022-02-09 01:55:53 +01:00
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 ] )
2020-10-31 23:34:06 +01:00
let rest : Partial < ImageProps > = all
2022-10-14 03:59:22 +02:00
let loader : ImageLoaderWithConfig = defaultLoader
2022-04-09 00:19:25 +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
2020-10-31 23:34:06 +01:00
}
2021-06-04 10:06:00 +02:00
let staticSrc = ''
2022-10-14 03:59:22 +02:00
let widthInt = getInt ( width )
let heightInt = getInt ( height )
let blurWidth : number | undefined
let blurHeight : number | undefined
2021-06-04 10:06:00 +02:00
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-10-14 03:59:22 +02:00
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
) } `
)
}
blurWidth = staticImageData . blurWidth
blurHeight = staticImageData . blurHeight
2021-06-10 20:51:35 +02:00
blurDataURL = blurDataURL || staticImageData . blurDataURL
2021-06-04 10:06:00 +02:00
staticSrc = staticImageData . src
2022-10-14 03:59:22 +02:00
if ( ! fill ) {
if ( ! widthInt && ! heightInt ) {
widthInt = staticImageData . width
heightInt = staticImageData . height
} else if ( widthInt && ! heightInt ) {
const ratio = widthInt / staticImageData . width
heightInt = Math . round ( staticImageData . height * ratio )
} else if ( ! widthInt && heightInt ) {
const ratio = heightInt / staticImageData . height
widthInt = Math . round ( staticImageData . width * ratio )
2021-06-04 10:06:00 +02:00
}
}
}
2021-06-09 00:05:02 +02:00
src = typeof src === 'string' ? src : staticSrc
2021-04-30 19:05:03 +02:00
2021-07-08 21:35:19 +02:00
let isLazy =
! priority && ( loading === 'lazy' || typeof loading === 'undefined' )
2021-08-14 00:03:20 +02:00
if ( src . startsWith ( 'data:' ) || src . startsWith ( 'blob:' ) ) {
2021-07-08 21:35:19 +02:00
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
unoptimized = true
isLazy = false
}
2022-09-01 00:44:17 +02:00
if ( config . unoptimized ) {
2022-06-16 22:20:17 +02:00
unoptimized = true
}
2021-07-08 21:35:19 +02:00
2022-04-06 00:47:55 +02:00
const [ blurComplete , setBlurComplete ] = useState ( false )
2022-10-14 03:59:22 +02:00
const [ showAltText , setShowAltText ] = useState ( false )
2022-03-14 15:25:23 +01:00
2022-07-20 23:26:38 +02:00
const qualityInt = getInt ( quality )
2020-10-22 20:59:42 +02:00
if ( process . env . NODE_ENV !== 'production' ) {
2020-10-26 15:29:52 +01:00
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.
unoptimized = true
} else {
2022-10-14 03:59:22 +02:00
if ( fill ) {
if ( width ) {
2022-07-20 23:26:38 +02:00
throw new Error (
2022-10-14 03:59:22 +02:00
` Image with src " ${ src } " has both "width" and "fill" properties. Only one should be used. `
)
}
if ( height ) {
throw new Error (
` Image with src " ${ src } " has both "height" and "fill" properties. Only one should be used. `
)
}
if ( style ? . position && style . position !== 'absolute' ) {
throw new Error (
` Image with src " ${ src } " has both "fill" and "style.position" properties. Images with "fill" always use position absolute - it cannot be modified. `
)
}
if ( style ? . width && style . width !== '100%' ) {
throw new Error (
` Image with src " ${ src } " has both "fill" and "style.width" properties. Images with "fill" always use width 100% - it cannot be modified. `
)
}
if ( style ? . height && style . height !== '100%' ) {
throw new Error (
` Image with src " ${ src } " has both "fill" and "style.height" properties. Images with "fill" always use height 100% - it cannot be modified. `
)
}
} else {
if ( typeof widthInt === 'undefined' ) {
throw new Error (
` Image with src " ${ src } " is missing required "width" property. `
)
} else if ( isNaN ( widthInt ) ) {
throw new Error (
` Image with src " ${ src } " has invalid "width" property. Expected a numeric value in pixels but received " ${ width } ". `
)
}
if ( typeof heightInt === 'undefined' ) {
throw new Error (
` Image with src " ${ src } " is missing required "height" property. `
)
} else if ( isNaN ( heightInt ) ) {
throw new Error (
` Image with src " ${ src } " has invalid "height" property. Expected a numeric value in pixels but received " ${ height } ". `
2022-07-20 23:26:38 +02:00
)
}
2022-06-27 17:11:15 +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' ) {
throw new Error (
` Image with src " ${ src } " has both "priority" and "loading='lazy'" properties. Only one should be used. `
)
}
2022-06-27 17:11:15 +02:00
2022-10-14 03:59:22 +02:00
if ( placeholder === 'blur' ) {
if ( widthInt && heightInt && widthInt * heightInt < 1600 ) {
2022-07-20 23:26:38 +02:00
warnOnce (
2022-10-14 03:59:22 +02:00
` Image with src " ${ src } " is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance. `
2022-07-20 23:26:38 +02:00
)
}
2022-10-14 03:59:22 +02:00
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.
2021-06-10 20:51:35 +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`
2021-10-29 00:32:10 +02:00
)
}
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. `
)
}
2021-10-25 23:59:00 +02:00
2022-10-14 03:59:22 +02:00
if ( ! unoptimized && loader !== defaultLoader ) {
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 `
2022-03-14 15:25:23 +01:00
)
}
2022-10-14 03:59:22 +02:00
}
2022-03-14 15:25:23 +01: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. ` +
` \ nRead more: https://nextjs.org/docs/api-reference/next/image#priority `
)
2021-10-25 23:59:00 +02:00
}
2022-07-20 23:26:38 +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-03-10 17:06:50 +01:00
}
2021-10-25 23:59:00 +02:00
}
2021-07-07 19:51:16 +02:00
}
2022-10-14 03:59:22 +02:00
const imgStyle = Object . assign (
fill
? {
position : 'absolute' ,
height : '100%' ,
width : '100%' ,
left : 0 ,
top : 0 ,
right : 0 ,
bottom : 0 ,
}
: { } ,
showAltText ? { } : { color : 'transparent' } ,
style
)
2021-07-20 03:23:16 +02:00
const blurStyle =
2022-10-14 03:59:22 +02:00
placeholder === 'blur' && blurDataURL && ! blurComplete
2021-04-30 19:05:03 +02:00
? {
2022-10-14 03:59:22 +02:00
backgroundSize : imgStyle.objectFit || 'cover' ,
backgroundPosition : imgStyle.objectPosition || '50% 50%' ,
backgroundRepeat : 'no-repeat' ,
backgroundImage : ` url("data:image/svg+xml;charset=utf-8, ${ getImageBlurSvg (
{
widthInt ,
heightInt ,
blurWidth ,
blurHeight ,
blurDataURL ,
}
) } " ) ` ,
2021-04-30 19:05:03 +02:00
}
2021-07-20 03:23:16 +02:00
: { }
2020-10-20 18:43:24 +02:00
2022-10-14 03:59:22 +02:00
if ( process . env . NODE_ENV === 'development' ) {
if ( blurStyle . backgroundImage && blurDataURL ? . startsWith ( '/' ) ) {
// During `next dev`, we don't want to generate blur placeholders with webpack
// because it can delay starting the dev server. Instead, `next-image-loader.js`
// will inline a special url to lazily generate the blur placeholder at request time.
blurStyle . backgroundImage = ` url(" ${ blurDataURL } ") `
}
2020-11-07 18:39:14 +01:00
}
2020-11-07 00:03:15 +01:00
2022-10-14 03:59:22 +02:00
const imgAttributes = generateImgAttrs ( {
config ,
src ,
unoptimized ,
width : widthInt ,
quality : qualityInt ,
sizes ,
loader ,
} )
2020-10-26 15:29:52 +01:00
2021-07-07 19:51:16 +02:00
let srcString : string = src
2021-10-25 23:59:00 +02:00
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 } )
}
}
2021-11-30 19:37:22 +01:00
let imageSrcSetPropName = 'imagesrcset'
let imageSizesPropName = 'imagesizes'
if ( process . env . __NEXT_REACT_ROOT ) {
imageSrcSetPropName = 'imageSrcSet'
imageSizesPropName = 'imageSizes'
}
2022-09-19 19:25:27 +02:00
const linkProps : React.DetailedHTMLProps <
React . LinkHTMLAttributes < HTMLLinkElement > ,
HTMLLinkElement
> = {
2021-11-30 19:37:22 +01:00
// Note: imagesrcset and imagesizes are not in the link element type with react 17.
[ imageSrcSetPropName ] : imgAttributes . srcSet ,
[ imageSizesPropName ] : imgAttributes . sizes ,
2022-09-19 19:25:27 +02:00
crossOrigin : rest.crossOrigin ,
2021-11-30 19:37:22 +01:00
}
2022-10-14 03:59:22 +02:00
const onLoadRef = useRef ( onLoad )
useEffect ( ( ) = > {
onLoadRef . current = onLoad
} , [ onLoad ] )
2022-01-20 16:29:34 +01:00
const onLoadingCompleteRef = useRef ( onLoadingComplete )
useEffect ( ( ) = > {
onLoadingCompleteRef . current = onLoadingComplete
} , [ onLoadingComplete ] )
2022-10-14 03:59:22 +02:00
const imgElementArgs : ImageElementProps = {
2022-03-14 15:25:23 +01:00
isLazy ,
imgAttributes ,
heightInt ,
widthInt ,
qualityInt ,
className ,
imgStyle ,
blurStyle ,
loading ,
config ,
2022-10-14 03:59:22 +02:00
fill ,
2022-03-14 15:25:23 +01:00
unoptimized ,
placeholder ,
loader ,
srcString ,
2022-10-14 03:59:22 +02:00
onLoadRef ,
2022-04-06 00:47:55 +02:00
onLoadingCompleteRef ,
setBlurComplete ,
2022-10-14 03:59:22 +02:00
setShowAltText ,
2022-03-14 15:25:23 +01:00
. . . rest ,
}
2020-10-14 11:57:10 +02:00
return (
2022-03-14 15:25:23 +01:00
< >
2022-10-14 03:59:22 +02:00
{ < ImageElement { ...imgElementArgs } / > }
2022-03-14 15:25:23 +01: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 >
2020-10-30 15:33:34 +01:00
) : null }
2022-03-14 15:25:23 +01:00
< / >
)
}