# 进行浏览器原生的图片懒加载的几种方式和原理
# 前言
对于图片较多的网站,倘若一次性加载所有图片,一方面由于同时加载的图片较多,页面的DOM
元素将非常多,会造成页面卡顿性能严重下降,另外服务器的压力也会很大。另一方面若加载了很多图片,而用户浏览的图片仅有几张,将会耗费大量流量,造成浪费。
而懒加载就是针对此情况做的优化,同时会极大地提升用户体验。一句话总结就是,懒加载即延时加载,当图片要用到的时候再去加载。
# offsetTop
懒加载的图片一般是固定宽高的,为避免图片较大时拉伸,可运用object-fit: cover
来裁剪。
<style>
img {
display: block;
margin-bottom: 10px;
width: 100%;
height: 200px;
object-fit: cover;
}
body {
margin: 0;
}
</style>
<img data-src="1.jpg" alt="">
<img data-src="2.jpg" alt="">
<img data-src="3.jpg" alt="">
<img data-src="4.jpg" alt="">
<img data-src="5.jpg" alt="">
<img data-src="6.jpg" alt="">
<img data-src="7.jpg" alt="">
其中loadImg
用来加载图片src
属性。
而滚动条频繁滚动会对浏览器性能有影响,因此封装debounce
防抖函数来限制触发频率。注意debounce
内部返回函数不能为箭头函数,会导致函数内this
指向改变,只有为普通函数,this
才能指向绑定事件的对象,例如el.addEventListener(event, fn)
,fn
内部this
应指向el
。
理论上图片位于视口就可以加载,但是为了提升用户体验,可以在图片距离视口固定距离就开始提前加载,因此全局定义了offset
偏移变量。
# 滚动条高度
;lazyLoad
函数中,window.innerHeight
为视口高度,document.documentElement.scrollTop
和document.body.scrollTop
都是获取滚动条滚动距离,两者差异主要取决于文档是否声明doctype
。
方式 | 类型 | Chrome | Firefox | IE11 | IE10 | IE9 |
---|---|---|---|---|---|---|
HTML 文档声明doctype | document.documentElement.clientHeight | 可获取 | 可获取 | 可获取 | 可获取 | 可获取 |
document.body.scrollTop | 0 | 0 | 0 | 0 | 0 | |
HTML 文档未声明doctype | document.documentElement.clientHeight | 0 | 0 | 可获取 | 可获取 | 可获取 |
document.body.scrollTop | 可获取 | 可获取 | 可获取 | 可获取 | 0 |
可以明显观察到document.documentElement.scrollTop
和document.body.scrollTop
中总有一个可以获取到滚动距离,因此可以document.documentElement.scrollTop || document.body.scrollTop
来兼容。
<script>
const loadImg = el => {
if (!el.src) {
el.src = el.dataset.src
}
}
const debounce = (fn, delay = 100) => {
var timer = null
return function (...args) {
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.call(this, ...args)
}, delay)
}
}
const imgs = document.querySelectorAll('img')
const offset = 20
var loaded = 0
const lazyLoad = () => {
const clientHeight = window.innerHeight
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
for (var i = loaded; i < imgs.length; i++) {
if (imgs[i].offsetTop <= clientHeight + scrollTop + offset) {
loadImg(imgs[i])
loaded++
} else {
break
}
}
}
lazyLoad()
window.addEventListener('scroll', debounce(lazyLoad, 200))
</script>
# loaded 变量
另外全局还定义了loaded
变量,用来存储图片即将加载的索引,以此避免每次从第一张图片开始遍历。
;for
循环体内if
语句为关键部分,只要图片的offset
属性小于视口高度、滚动距离与偏移值之和,则必然加载图片。某张图片不满足加载条件,则后续图片必然也不满足,因此break
提前终止循环。
# getBoundingClientRect
;getBoundingClientRect (opens new window) 用于返回元素的大小及相对于视口的位置。
浏览器兼容性方面,Chrome
、Firefox
和IE5
及以上浏览器等均兼容。
标准盒模型,元素的宽高尺寸为width/height + padding + border-width
总和。若其CSS
属性为box-sizing: border-box
,则元素尺寸为width/height
。
#img {
display: block;
margin-bottom: 10px;
width: 300px;
height: 200px;
border: 10px solid lightblue;
padding: 20px;
}
<img id="img" src="image.png" alt="">
const img = document.getElementById('img')
console.log(img.getBoundingClientRect())
# 浏览器差异
;Chrome
浏览器打印参数。
;IE8
浏览器打印参数,注意IE8
及以下浏览器返回的对象中不含width
、height
属性。
;IE7
浏览器打印参数,注意IE7
浏览器中的页面内的HTML
元素的坐标会从(2, 2)
开始计算。
因此封装为工具函数,兼容IE7
及以上浏览器。
function getBoundingClientRect(el) {
var rect = el.getBoundingClientRect()
var l = document.documentElement.clientLeft
var t = document.documentElement.clientTop
return {
left: rect.left - l,
right: rect.right - l,
bottom: rect.bottom - t,
top: rect.top - t,
width: rect.right - rect.left,
height: rect.bottom - rect.top,
}
}
根据此工具函数,针对offsetTop
方式的懒加载稍作修改。
const lazyLoad = () => {
for (var i = loaded; i < imgs.length; i++) {
if (getBoundingClientRect(imgs[i]).top <= window.innerHeight + offset) {
loadImg(imgs[i])
loaded++
} else {
break
}
}
}
# IntersectionObserver
;IntersectionObserver (opens new window) 是浏览器提供的构造函数,用于创建一个观察器实例,详细参考 (opens new window)。
const io = new IntersectionObserver(callback, options)
此实例提供了部分方法。
io.observe()
:开始观察,参数为某个DOM
节点对象io.unobserve()
:取消观察,参数可为DOM
节点对象,也可不传io.disconnect()
:关闭观察器
再来看看callback
回调函数,一般是视窗观察某个或多个元素,且callback
通常会触发两次,一次是被观察元素刚进入视口时,另一次是被观察元素完全离开视口时。
const io = new IntersectionObserver((entries, observer) => { })
# IntersectionObserverEntry
;observer
为被调用的IntersectionObserver
实例,即上述io
实例。
;entries
是一个 IntersectionObserverEntry (opens new window) 对象数组。若视窗观察了3
个元素,则entries
数组内就会有3
个实例,且均是IntersectionObserverEntry
对象。
;Chrome
浏览器下IntersectionObserverEntry
对象包括8
个属性。
boundingClientRect
:被观察元素的矩形信息,即被观察元素执行el.getBoundingClientRect()
的返回结果intersectionRect
:被观察元素与视窗(或者根元素)的相交区域的矩形信息intersectionRatio
:相交比例,即intersectionRect
占boundingClientRect
面积的比例,被观察元素完全可见时为1
,完全不可见时为0
isIntersecting
:被观察元素是否在视窗中可见,可见则为true
rootBounds
:根元素矩形信息,未指定根元素则为视窗的矩形信息target
:被观察元素,是一个DOM
节点time
:高精度时间戳,单位为毫秒。表示从IntersectionObserver
的时间原点到callback
被触发时两者之间的时间长度
构造函数IntersectionObserver
的第二个参数options
是一个对象,主要包括三个属性。
threshold
:即被观察元素在视口中可见部分为多少时,触发回调函数,threshold
为一个数组,默认为[0]
如下被观察元素有0%
、50%
、75%
、100%
可见部分时,触发回调函数
new IntersectionObserver(callback, {
threshold: [0, 0.5, 0.75, 1],
})
root
:除了支持观察视窗内元素,也支持指定根元素
如下ul
元素内部多个li
滚动时,某个li
出现在ul
时触发。
<style>
ul {
width: 300px;
height: 100px;
overflow: auto;
}
li {
height: 24px;
background-color: #ccc;
margin-bottom: 1px;
}
li:nth-of-type(9) {
background-color: lightblue;
}
</style>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
<script>
const ul = document.querySelector('ul')
const li = document.querySelectorAll('li')[8]
const callback = entries => {
console.log(entries)
}
const io = new IntersectionObserver(callback, {
root: ul,
})
io.observe(li)
</script>
注意根元素必须为被观察元素的祖先元素。
rootMargin
:定义视窗或者根元素的margin
,用于拓展rootBounds
区域的大小,默认值为"0px 0px 0px 0px"
如下视窗被拓展为红色区域部分,一般被观察元素仅在视窗中出现(或者出现指定比例)才会触发,若要被观察元素在距离视窗固定距离就提前触发,rootMargin
则可派上用场了。
# 实现部分
现在来看图片懒加载的情况,代码比较少,先看代码。
<script>
const loadImg = el => {
if (!el.src) {
el.src = el.dataset.src
}
}
const offset = 20
const imgs = document.querySelectorAll('img')
const callback = (entries, i) => {
entries.forEach(el => {
if (el.isIntersecting) {
loadImg(el.target)
io.unobserve(el.target)
}
})
}
const io = new IntersectionObserver(callback, {
rootMargin: `0px 0px ${offset}px 0px`,
})
imgs.forEach(img => io.observe(img))
</script>
首先创建观察器io
,由于未指定根元素,所以默认为视窗,然后视窗遍历观察img
元素。
还是和offsetTop
方式一致,距离视口20px
就提前加载图片。因此添加rootMargin
配置项。
;callback
回调函数部分,元素只要出现在视口,则加载图片,同时unobserve
取消观察对应的img
元素。
# 兼容性
以上对于Chrome
或者Firefox
等浏览器是完全可用的,对于IE9-11
是不兼容的,利用 intersection-observer-polyfill (opens new window) 插件来兼容一波吧。
注意IE
浏览器不支持object-fit
样式,但是不是重点,不过多详述,感兴趣可以自己捣鼓。
<script src="IntersectionObserver.js"></script>
<style>
img {
display: block;
margin-bottom: 10px;
width: 100%;
height: 200px;
/* object-fit: cover; */
}
body {
margin: 0;
}
</style>
<script>
var loadImg = function (el) {
if (!el.src) {
el.src = el.getAttribute('data-src')
}
}
var offset = 20
var imgs = document.getElementsByClassName('aaa')
var callback = function (entries, i) {
entries.forEach(function (el) {
if (el.isIntersecting || el.intersectionRatio > 0) {
loadImg(el.target)
io.unobserve(el.target)
}
})
}
var io = new IntersectionObserver(callback, {
rootMargin: '0px 0px ' + offset + 'px 0px',
})
for (var i = 0; i < imgs.length; i++) {
io.observe(imgs[i])
}
</script>
;IE9
浏览器下效果。
# 🎉 写在最后
🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!
手动码字,如有错误,欢迎在评论区指正💬~
你的支持就是我更新的最大动力💪~
GitHub (opens new window) / Gitee (opens new window)、GitHub Pages (opens new window)、掘金 (opens new window)、CSDN (opens new window) 同步更新,欢迎关注😉~