Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby-image): Image cache logic improvements #26090

Closed
95 changes: 63 additions & 32 deletions packages/gatsby-image/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,6 @@ class Image extends React.Component {
if (this.state.isVisible && typeof this.props.onStartLoad === `function`) {
this.props.onStartLoad({ wasCached: inImageCache(this.props) })
}
if (this.isCritical) {
const img = this.imageRef.current
if (img && img.complete) {
this.handleImageLoaded()
}
}
}

componentWillUnmount() {
Expand All @@ -378,42 +372,74 @@ class Image extends React.Component {
}
}

// Specific to IntersectionObserver based lazy-load support
// Triggers when wrapper element mounts(has a ref) and after unmounting(null)
handleRef(ref) {
if (this.useIOSupport && ref) {
// Ignore calls from unmounts
if (!ref) {
return
}

// If the image can be detected in the browser already,
// this will reveal it immediately and skip the placeholder transition.
const setCachedState = () => {
// 'img.currentSrc' may be a falsy value, empty string or undefined(IE).
if (!this.imageRef.current) {
return
}
// If image resides in the browser cache, this attribute is populated
// without waiting on a network request response to update it.
// Firefox does not behave in this way, no benefit there.
let isCached = this.imageRef.current.currentSrc

// For critical images 'img.complete' is more reliable
if (this.isCritical) {
isCached = this.imageRef.current.complete
}

if (isCached) {
this.setState({
imgCached: true,
})
}
}

// Newer instances(mounts) of this image may be cached, using a different
// render path than Intersection Observer ('useIOSupport').
// 'isVisible' guard skips this for already loaded art directed images.
if (this.useIOSupport && !this.state.isVisible) {
this.cleanUpListeners = listenToIntersections(ref, () => {
const imageInCache = inImageCache(this.props)
if (
!this.state.isVisible &&
typeof this.props.onStartLoad === `function`
) {

if (typeof this.props.onStartLoad === `function`) {
this.props.onStartLoad({ wasCached: imageInCache })
}

// imgCached and imgLoaded must update after isVisible,
// Once isVisible is true, imageRef becomes accessible, which imgCached needs access to.
// imgLoaded and imgCached are in a 2nd setState call to be changed together,
// avoiding initiating unnecessary animation frames from style changes.
this.setState({ isVisible: true }, () => {
// 'isVisible' enables loading the real image.
// If internally cached, reveal image immediately, skipping transition.
if (imageInCache) {
this.setState({
imgLoaded: imageInCache,
// `currentSrc` should be a string, but can be `undefined` in IE,
// !! operator validates the value is not undefined/null/""
// for lazyloaded components this might be null
// TODO fix imgCached behaviour as it's now false when it's lazyloaded
imgCached: !!(
this.imageRef.current && this.imageRef.current.currentSrc
),
isVisible: true,
imgCached: true,
})
})
} else {
// Begin loading real image, then check if image is in browser cache.
// Must first render with 'isVisible==true', then 'imageRef' exists.
// 'imgCached' needs access to 'imageRef' to work.
// Paired with 'imgLoaded' to avoid redundant animation frames.
this.setState({ isVisible: true }, setCachedState)
}
})
} else if (!this.state.imgCached) {
setCachedState()
}
}

handleImageLoaded() {
activateCacheForImage(this.props)

this.setState({ imgLoaded: true })
if (!this.state.imgLoaded) {
this.setState({ imgLoaded: true })
}

if (this.props.onLoad) {
this.props.onLoad()
Expand Down Expand Up @@ -442,6 +468,11 @@ class Image extends React.Component {
const shouldReveal = this.state.fadeIn === false || this.state.imgLoaded
const shouldFadeIn = this.state.fadeIn === true && !this.state.imgCached

// In a browser-cached scenario prevents rendering initial placeholder.
// Instead renders a blank image container or backgroundColor placeholder.
const shouldRenderPlaceholder = !isBrowser || this.state.isVisible
const shouldHidePlaceholder = this.state.imgCached || this.state.imgLoaded

const imageStyle = {
opacity: shouldReveal ? 1 : 0,
transition: shouldFadeIn ? `opacity ${durationFadeIn}ms` : `none`,
Expand All @@ -456,7 +487,7 @@ class Image extends React.Component {
}

const imagePlaceholderStyle = {
opacity: this.state.imgLoaded ? 0 : 1,
opacity: shouldHidePlaceholder ? 0 : 1,
...(shouldFadeIn && delayHideStyle),
...imgStyle,
...placeholderStyle,
Expand Down Expand Up @@ -515,7 +546,7 @@ class Image extends React.Component {
)}

{/* Show the blurry base64 image. */}
{image.base64 && (
{shouldRenderPlaceholder && image.base64 && (
<Placeholder
ariaHidden
ref={this.placeholderRef}
Expand All @@ -527,7 +558,7 @@ class Image extends React.Component {
)}

{/* Show the traced SVG image. */}
{image.tracedSVG && (
{shouldRenderPlaceholder && image.tracedSVG && (
<Placeholder
ariaHidden
ref={this.placeholderRef}
Expand Down Expand Up @@ -618,7 +649,7 @@ class Image extends React.Component {
)}

{/* Show the blurry base64 image. */}
{image.base64 && (
{shouldRenderPlaceholder && image.base64 && (
<Placeholder
ariaHidden
ref={this.placeholderRef}
Expand All @@ -630,7 +661,7 @@ class Image extends React.Component {
)}

{/* Show the traced SVG image. */}
{image.tracedSVG && (
{shouldRenderPlaceholder && image.tracedSVG && (
<Placeholder
ariaHidden
ref={this.placeholderRef}
Expand Down