实现图片懒加载
前言
本文会分为以下阶段来讲解为何图片需要懒加载,以及如何实现图片懒加载组件:
- 懒加载的意义所在
- 懒加载原理
- 原生JS实现
- 实现一个完善的懒加载组件
懒加载的意义所在
一般来说,一个页面的内容,很大部分由图片来组成,而图片往往会很大。一张精细的图片,很容易达到几百KB,甚至几M,而代码也许就只有几十KB。当页面图片丰富时,往往影响页面加载速度的都是图片资源,当存在大量图片或者图片很大时,页面的加载速度会很缓慢,这当然会影响用户体验。
因此,针对这个问题,为了使页面加载速度变快,我们会思考,当图片不在屏幕中时,不去加载图片资源,当图片出现在视图中再去加载图片资源,从而提升关健资源加载速度,避免浪费浏览器的效率,使完整界面呈现在用户面前的时间变得更快,从而优化用户体验。
懒加载原理
懒加载的原理核心就是:在图片即将出现在屏幕中时再去加载图片资源。
假设我们把图片资源存储在 img
的一个名为 data-lazy-src
属性上,当浏览器监听到图片出现、或即将出现在屏幕中时,获取图片的data-lazy-src
属性值,并赋值给 img
的 src
属性,这时候后浏览器就会去加载 src
上的图片资源,并显示图片。这样就实现了图片的懒加载。
原生JS实现
实现懒加载的方式有不少,下面我主要介绍两种:
- 用
img
图片的offectTop
以及屏幕的可见区域宽度
、页面内容滚动的高度scrollTop
来结合计算图片是否出现在屏幕的可见区域
来实现 src 的赋值,这种方式的具体实现可以去看这一篇文章:实现图片懒加载 (opens in a new tab) - 使用
IntersectionObserver
来监听图片
出现或即将出现在屏幕的可见区域
来实现懒加载。IntersectionObserver MDN文档 (opens in a new tab)
这里我就主要讲解 IntersectionObserver
的方式。代码如下:
<!DOCTYPE html>
<html>
<head>
<title>原生JS实现图片懒加载</title>
<style>
img {
display: block;
height: 500px;
}
</style>
</head>
<body>
<div>
<img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20190216/f9955cfc7f0b42919ae3f58138e94bc2.jpeg" />
<img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20190216/f9955cfc7f0b42919ae3f58138e94bc2.jpeg" />
<img data-lazy-src="https://image.uc.cn/s/wemedia/s/upload/2020/cfb49108038258406649e2d59cf7630e.jpg" />
<img data-lazy-src="https://image.uc.cn/s/wemedia/s/upload/2020/cfb49108038258406649e2d59cf7630e.jpg" />
<img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20200505/e225141aa9a04509a5e6b86cc43fdec8.png" />
<img data-lazy-src="https://5b0988e595225.cdn.sohucs.com/images/20200505/e225141aa9a04509a5e6b86cc43fdec8.png" />
<img data-lazy-src="https://n.sinaimg.cn/sinacn10121/708/w900h608/20190815/6683-ichcymv6468668.jpg" />
<img data-lazy-src="https://p3.pstatp.com/large/pgc-image/1533130159281fe21849267" />
</div>
<script>
// 实例化IntersectionObserver
const intersectionObserver = new IntersectionObserver(observerCallback, {
rootMargin: "100px 0px 100px" // 当不超出屏幕可见范围上下100px内时
});
// 给所有带有data-lazy-src属性的图片加入intersectionObserver的观察范围
const imgs = document.querySelectorAll("img");
for (let i = 0; i < imgs.length; i += 1) {
const { dataset } = imgs[i];
// dataset带有lazySrc属性时才加入观察返回
if (dataset.lazySrc) {
intersectionObserver.observe(imgs[i]);
}
}
// 监听回调
function observerCallback(entries, observer) {
// 遍历
Array.from(entries).forEach((item) => {
// 在定义的视图范围内时(rootMargin范围内)才进行加载新图片
if (item.isIntersecting && item.target.dataset.lazySrc) {
item.target.src = item.target.dataset.lazySrc;
// 对已经加载的图片取消观察
intersectionObserver.unobserve(item.target);
}
});
}
</script>
</body>
</html>
预览效果:原生JS实现懒加载-IntersectionObserver方式 (opens in a new tab)
调试代码:https://codesandbox.io/s/fervent-rain-54vo9?file=/lazyImage.html (opens in a new tab)
实现一个较完善的 React 懒加载组件
图片有使用 img 标签的情况,也有作为背景图使用的情况,如果要做一个通用的LazyImage组件,就需要考虑到这些。
对组件添加一个 tagType
属性来处理这种情况,当 tagType
为 img
,就渲染出 img
标签;
当 tagType
为 div
时,就渲染出 div
标签,这时候就可以给组件传递 children
,这时候需要注意一定需要设置图片的宽高(style或者className的方式都可以)。
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
// 图片加载状态
export const LoadStatus = {
notLoad: 0, // 未加载
loadSuccess: 1, // 加载成功
loadFail: 2 // 加载失败
};
const image_preview = "xxx"; // 预览图地址
const image_fail = "xxx"; // 预览图地址
function LazyImage(props) {
// 图片显示的地址,默认显示预览图
const [currentSrc, setCurrentSrc] = useState(image_preview);
// 图片状态,默认为`未加载`状态
const [status, setStatus] = useState(LoadStatus.notLoad);
// 用于获取图片的DOM实例
const imgRef = useRef();
const { tagType, src, alt, className, onLoad, style, ...otherProps } = props;
useEffect(() => {
// 当图片地址不存在时
if (!src) {
// 显示预览图
setCurrentSrc(image_preview);
return;
}
// 每次图片地址改变都重置状态、重置未预览图
if (currentSrc !== image_preview) { // 第一次不需要再进行重置
setStatus(LoadStatus.notLoad);
setCurrentSrc(image_preview);
}
// 监听回调
function observerCallback(entries, observer) {
// 在定义的视图范围内时(rootMargin范围内)才进行加载新图片
if (entries[0].isIntersecting) {
// 预加载图片
const img = new Image();
img.onload = () => {
// 图片预加载完再进行设置真实图片地址
setCurrentSrc(props.src);
setStatus(LoadStatus.loadSuccess);
props.onLoad && props.onLoad(LoadStatus.loadSuccess);
};
img.onerror = () => {
// 图片预加载失败则显示加载失败图
setCurrentSrc(image_fail);
setStatus(LoadStatus.loadFail);
props.onLoad && props.onLoad(LoadStatus.loadFail);
};
// 开始加载图片
img.src = props.src;
// 对已经加载的图片取消观察
intersectionObserver.unobserve(imgRef.current);
}
}
// 实例化IntersectionObserver
const intersectionObserver = new IntersectionObserver(observerCallback, {
rootMargin: "100% 0px 100%" // 当不超出屏幕可见范围上下一屏时
});
if (imgRef.current) {
// 加入IntersectionObserver观察
intersectionObserver.observe(imgRef.current);
}
return () => {
// 注销
intersectionObserver.disconnect();
};
}, [src]);
const imgProps = {
...otherProps,
ref: imgRef,
className: classnames("lazy-img", className, {
"lazy-img-not-load": status === LoadStatus.notLoad,
"lazy-img-load-fail": status === LoadStatus.loadFail,
"lazy-img-load-success": status === LoadStatus.loadSuccess
})
};
// 如果不想用img标签时,图片就作为背景图来显示图片
// 这时候需要设置图片宽高,否则图片不会正常显示
if (tagType === "div") {
return (
<div
{...imgProps}
style={{
...style,
backgroundImage: `url(${currentSrc})`
}}
/>
);
}
return <img {...imgProps} style={style} alt={alt} src={currentSrc} />;
}
LazyImage.propTypes = {
tagType: PropTypes.oneOf(["img", "div"]),
src: PropTypes.string,
alt: PropTypes.string,
onLoad: PropTypes.func,
className: PropTypes.string,
style: PropTypes.object
};
LazyImage.defaultProps = {
tagType: "div"
};
export default LazyImage;
预览效果:React 懒加载图片组件 (opens in a new tab)
调试代码:https://codesandbox.io/s/react-demo-6j9y3?file=/src/LazyImage.js (opens in a new tab)
上面我们实现了一个相等完整的懒加载图片组件。但还有一些问题需要考虑到:
- 预览图大小问题,在
tagType
为img
时,当前预览图或者失败图大小就等于真实图片的尺寸。(可以考虑在预览和加载失败情况下返回div标签,用背景图显示预览图或者其他方式来保证预览图、失败图的效果) - 图片宽度高度的问题,在
tagType
为div
时,须预先设定图片的宽高,否则图片预览时就和真实图片的大小不一致。 - 如果图片的宽高信息在图片url参数上,可以在懒加载图片组件内部进行转换获取
- webp图片支持,这就涉及具体场景了。
可能还有其他问题,具体场景再进行具体处理。
最后
有问题的地方欢迎指出,感谢!