Last active
February 4, 2024 12:38
-
-
Save oauo/9bd7c0d391237874fcc9c8c216c86c4d to your computer and use it in GitHub Desktop.
Medium style lazy loading images with react #codepen #react
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
div#app |
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
{ | |
"scripts": [ | |
"react", | |
"react-dom", | |
"https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.6/index.min.js" | |
], | |
"styles": [ | |
"font-awesome", | |
"https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" | |
] | |
} |
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
class LazyImage extends React.Component { | |
is_cached = src => { | |
//Cached images don't flash and get shown immediately | |
let image = new Image(); | |
image.src = src; | |
return image.complete; | |
}; | |
constructor(props) { | |
super(props); | |
this.element = React.createRef(); | |
this.state = { | |
try: false, | |
visible: false, | |
shown: this.is_cached(this.props.main), | |
error: false | |
}; | |
} | |
componentDidMount() { | |
this.observer = new IntersectionObserver(this.handleVisibilityChange, { | |
threshold: 0.5 | |
}); | |
this.observer.observe(this.element.current); | |
this.setState({ try: true }); | |
} | |
imageLoaded = () => { | |
this.setState({ shown: true, error: false }); | |
}; | |
imageError = () => { | |
if (!this.state.shown) this.setState({ error: true }); | |
}; | |
handleVisibilityChange = e => { | |
if (e[0].intersectionRatio > 0) { | |
this.setState({ visible: true }); | |
this.observer.unobserve(this.element.current); | |
} | |
}; | |
render() { | |
return ( | |
<div | |
ref={this.element} | |
className="lazyimage" | |
style={{ "--height": this.props.height * 100 + "%" }} | |
> | |
<img | |
className={classNames("image", "thumb")} | |
src={this.props.thumb} | |
alt={this.props.alt} | |
onError={this.imageError} | |
/> | |
{(this.state.shown || (this.state.try && this.state.visible)) && ( | |
<img | |
className={classNames("image", "main", { | |
show: this.state.shown | |
})} | |
src={this.props.main} | |
alt={this.props.alt} | |
onError={this.imageError} | |
onLoad={this.imageLoaded} | |
/> | |
)} | |
{this.state.error && ( | |
<div | |
className="error" | |
title={`Image could not be loaded. ${(this.props.alt || "")}`} | |
> | |
<i class="fas fa-exclamation-circle"></i> | |
</div> | |
)} | |
</div> | |
); | |
} | |
} | |
class LazyImageMod extends LazyImage { | |
/* | |
Only used for demo, do not use this for anything proper | |
With this element, the image cache is ignored and there is a delay until the image unblurs | |
*/ | |
constructor(props) { | |
super(props); | |
this.props.thumb += "?" + Math.floor(Math.random() * 1000000); | |
this.props.main += "?" + Math.floor(Math.random() * 1000000); | |
this.state.shown = false; //🙈 | |
} | |
imageLoaded = () => { | |
setTimeout(() => { | |
this.setState({ shown: true }); | |
}, this.props.delay || 0); | |
}; | |
} | |
const App = () => ( | |
<div id="container"> | |
<h1>Medium style lazy image loader with react</h1> | |
<h2>Normal</h2> | |
<p> | |
Loads blurred low resolution thumbnail, waits until page loads and image | |
is on the screen before loading the main image which then unblurs to | |
reveal the main image. | |
</p> | |
<p> | |
Image will not blur if it is in the device cache. The other examples on | |
this page have cache check disabled and will flash a blur. | |
</p> | |
<LazyImage | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade.jpg" | |
alt="woman leaning against monitor displaying Pacman game" | |
height={540 / 1260} | |
/> | |
<h2>Delayed loading & skip cache check</h2> | |
<p>This image is delayed by 1 second</p> | |
<LazyImageMod | |
delay={1000} | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade.jpg" | |
alt="woman leaning against monitor displaying Pacman game" | |
height={540 / 1260} | |
/> | |
<h2>Scroll to start loading, no delay</h2> | |
<p> | |
Once image is 50% in view it loads (customisable on line 20), images are{" "} | |
<b>not</b> delayed. | |
</p> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/concrete-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/concrete.jpg" | |
alt="man and woman sitting on floor" | |
height={630 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/road-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/road.jpg" | |
alt="woman walking in the middle of the road" | |
height={480 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade.jpg" | |
alt="woman leaning against monitor displaying Pacman game" | |
height={540 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/concrete-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/concrete.jpg" | |
alt="man and woman sitting on floor" | |
height={630 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/road-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/road.jpg" | |
alt="woman walking in the middle of the road" | |
height={480 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade.jpg" | |
alt="woman leaning against monitor displaying Pacman game" | |
height={540 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/concrete-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/concrete.jpg" | |
alt="man and woman sitting on floor" | |
height={630 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/road-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/road.jpg" | |
alt="woman walking in the middle of the road" | |
height={480 / 1260} | |
/> | |
<LazyImageMod | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/arcade.jpg" | |
alt="woman leaning against monitor displaying Pacman game" | |
height={540 / 1260} | |
/> | |
<a className="imageCredit" href="https://www.instagram.com/everydaysafilm/"> | |
<i className="fab fa-instagram" /> Images from @everydaysafilm | |
</a> | |
<h2>Error</h2> | |
<p>Oops, the main image path isn't right.</p> | |
<LazyImage | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/error-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/errror.jpg" | |
height={540 / 1260} | |
/> | |
<LazyImage | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/error-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/errror.jpg" | |
alt="Red 'ERROR' on white background." | |
height={540 / 1260} | |
/> | |
<p>Main image is ok, but the thumbnail isn't now.</p> | |
<LazyImage | |
thumb="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/errror-thumb.jpg" | |
main="https://s3-us-west-2.amazonaws.com/s.cdpn.io/199011/error.jpg" | |
alt="Red 'ERROR' on white background." | |
height={540 / 1260} | |
/> | |
</div> | |
); | |
ReactDOM.render(<App />, document.getElementById("app")); |
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
body { | |
display: flex; | |
flex-direction: column; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin:2rem 4rem; | |
color:black; | |
#container { | |
max-width:40rem; | |
margin:auto; | |
} | |
.imageCredit { | |
color:white; | |
background-color: black; | |
text-decoration: none; | |
padding:.4rem .75rem .5rem .75rem; | |
border-radius: 2rem; | |
} | |
h2 { | |
padding-top:1rem; | |
&:nth-of-type(n+2) { | |
border-top: 1px solid #E6E9ED; | |
} | |
} | |
.lazyimage { | |
position: relative; | |
background:#E6E9ED; | |
border-radius: 1rem; | |
overflow: hidden; | |
margin-bottom: 2rem; | |
&.visible { | |
box-shadow:inset 0 2rem 0 0 black; | |
} | |
&:before { | |
content: ''; | |
display: block; | |
margin-top: var(--height, 100%); | |
} | |
.image { | |
position: absolute; | |
top:0; | |
left:0; | |
background-size: cover; | |
background-position: center; | |
&.thumb, &.main { | |
filter: blur(.5rem); | |
width:100%; | |
} | |
&.thumb { | |
z-index: 0; | |
} | |
&.main { | |
z-index: 1; | |
opacity:0; | |
transition:opacity .25s, filter .25s .15s; | |
&.show { | |
opacity:1; | |
filter:blur(0.01rem); //Setting to 0 makes it jump a bit sometimes | |
} | |
} | |
} | |
.error { | |
z-index:5; | |
position: absolute; | |
bottom:.5rem; | |
right:.5rem; | |
color:black; | |
font-size:2rem; | |
text-shadow: 0 0 1rem white; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment