【W(wǎng)eb技術】906- 徹底玩轉圖片懶加載及底層實現(xiàn)原理
前言
圖片懶加載其實已經(jīng)是一個近乎“爛大街”的詞語了,在大大小小的面試中也會被頻繁的問到,我在之前的面試中也被問到了圖片懶加載的原因、實現(xiàn)方式及底層原理,但由于自己平時很少做“圖片”相關的處理,對于“懶加載”也是知之甚少,所以在面試中問答的也不是很好。
今天,我將首先從瀏覽器底層渲染機制來剖析為什么要去做圖片懶加載,之后我將帶大家一起來看下目前主流的幾種實現(xiàn)圖片懶加載的方式及其實現(xiàn)原理,最后會做一個展望。
為什么要做圖片懶加載
要問答這個問題,首先我們先來看下瀏覽器的底層渲染機制:
1、構建 DOM 樹
2、樣式計算
3、布局階段
4、分層
5、繪制
6、分塊
7、光柵化
8、合成
而在構建DOM的過程中如果遇到img在新老版本的chrome中表現(xiàn)又是不一樣的:
老版本:阻塞 DOM 渲染 新版本:雖然不會阻塞 DOM 渲染,但每一個圖片請求都會占用一個 HTTP,而且 Chrome 最多允許對同一個 Host 同時建立六個 TCP 連接
當你打開一個網(wǎng)站時,瀏覽器會做許多工作,這其中包括下載各種可能用到的資源,然后渲染呈現(xiàn)在你面前,假設你的網(wǎng)站有大量的圖片,那么加載的過程是很耗時的,尤其像那些電商類需要大量圖片的網(wǎng)站,可想而知,網(wǎng)站的初始加載時間會很長,再加上網(wǎng)絡等其它影響,用戶體驗會很差。
相信你經(jīng)常遇到過一個網(wǎng)站卡在某個地方,一直在加載,這種體驗很不好。我們都希望一輸入網(wǎng)址,頁面立馬就呈現(xiàn)在眼前。
總結一下就是:直接全部加載的話會減緩渲染速度,產(chǎn)生白屏等進而影響用戶體驗。
基于原生 js 實現(xiàn)圖片懶加載
相關 API
先來看幾個后面會用到的API
document.documentElement.clientHeight
獲取屏幕可視區(qū)域的高度。

“圖片來源MDN[1]
element.offsetTop
獲取元素相對于文檔頂部的高度。

“圖片來源阮一峰博客[2]
document.documentElement.scrollTop
獲取瀏覽器窗口頂部與文檔頂部之間的距離,也就是滾動條滾動的距離。

“圖片來源Seven's Blog
思路分析
通過上面三個 API,我們獲得了三個值:可視區(qū)域的高度、元素相對于其父元素容器頂部的距離、瀏覽器窗口頂部與容器元素頂部的距離也就是滾動條滾動的高度。
雖然這幾個API很簡單,但是單純的去說還是有點抽象,這里我們還是用圖來展示一下:
看完上面這張圖片,我想你已經(jīng)明白了:如果滿足offsetTop-scroolTop<clientHeight,則圖片進入了可視區(qū)內,我們就去請求進入可視區(qū)域的圖片。
代碼實現(xiàn)
基于上面的分析,我們很容易就可以寫出如下代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>基于原生 js 實現(xiàn)圖片懶加載</title>
<style>
img {
display: block;
width: 100%;
height: 300px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<img src="./img/1.png" alt="">
<img src="./img/2.png" alt="">
<img src="./img/3.png" alt="">
<img src="./img/4.png" alt="">
<img src="./img/5.png" alt="">
<img src="./img/6.png" alt="">
<img src="./img/7.png" alt="">
<img src="./img/8.png" alt="">
</body>
<script>
var imgs = document.querySelectorAll('img');
//offsetTop是元素與offsetParent的距離,循環(huán)獲取直到頁面頂部
function getRealTop(e) {
var realTop = e.offsetTop;
while(e = e.offsetParent) {
realTop += e.offsetTop;
}
return realTop;
}
function lazyLoad(imgs) {
var H = document.documentElement.clientHeight;//獲取可視區(qū)域高度
var S = document.documentElement.scrollTop || document.body.scrollTop;
for (var i = 0; i < imgs.length; i++) {
if (H + S > getRealTop(imgs[i])) {
imgs[i].src = imgs[i].getAttribute('src');
}
}
}
window.onload = window.onscroll = function () { //onscroll()在滾動條滾動的時候觸發(fā)
lazyLoad(imgs);
}
</script>
</html>
“但上面的代碼如果你在
lazyLoad中打印,你會發(fā)現(xiàn)滾動條上下滾動時,lazyLoad會被頻繁調用,造成很大的性能損失,這里我們可以給事件加上節(jié)流throttle
基于 getBoundingClientRect()實現(xiàn)圖片懶加載
先來了解一下這個API吧:
getBoundingClientRect()用于獲得頁面中某個元素的左,上,右和下分別相對瀏覽器視窗的位置。getBoundingClientRect()是DOM元素到瀏覽器可視范圍的距離(不包含頁面看不見的部分)。
該函數(shù)返回一個rectObject對象,該對象有 6 個屬性:top, left, bottom, right, width, height;這里的top、left和css中的理解很相似,width、height是元素自身的寬高,但是right,bottom和css中的理解有點不一樣。right是指元素右邊界距窗口最左邊的距離,bottom是指元素下邊界距窗口最上面的距離。

思路分析
通過這個 API,我們就很容易獲取img元素相對于視口的頂點位置rectObject.top,只要這個值小于瀏覽器的高度window.innerHeight就說明進入可視區(qū)域:
function isInSight(el){
const bound = el.getBoundingClientRect();
const clientHeight = window.innerHeight;
return bound.top <= clientHeight;
}
代碼實現(xiàn)
這里結合第一種實現(xiàn)方式,做下改造,就得到了:
function loadImg(el){
if(!el.src){
const source = el.getAttribute('src');;
el.src = source;
}
}
function checkImgs(){
const imgs = document.querySelectorAll('img');
Array.from(imgs).forEach(el =>{
if (isInSight(el)){
loadImg(el);
}
})
}
window.onload = function(){
checkImgs();
}
document.onscroll = function () {
checkImgs();
}
基于 IntersectionObserver 實現(xiàn)圖片懶加載
概念
同樣,還是先來看一下概念。
“這里我們參考阮一峰大佬關于IntersectionObserver API[3]的介紹。
我們在平時的開發(fā)中,常常需要了解某個元素是否進入了"視口"(viewport),即用戶能不能看到它。

上圖的綠色方塊不斷滾動,頂部會提示它的可見性。
傳統(tǒng)的實現(xiàn)方法是,監(jiān)聽到scroll事件后,調用目標元素(綠色方塊)的getBoundingClientRect()方法,得到它對應于視口左上角的坐標,再判斷是否在視口之內。這種方法的缺點是,由于scroll事件密集發(fā)生,計算量很大,容易造成性能問題。
目前有一個新的 IntersectionObserver API,可以自動"觀察"元素是否可見,Chrome 51+ 已經(jīng)支持。由于可見(visible)的本質是,目標元素與視口產(chǎn)生一個交叉區(qū),所以這個 API 叫做交叉觀察器。
使用
它的用法也非常簡單。
var io = new IntersectionObserver(callback, option);
上面代碼中,IntersectionObserver是瀏覽器原生提供的構造函數(shù),接受兩個參數(shù):callback是可見性變化時的回調函數(shù),option是配置對象(該參數(shù)可選)。
構造函數(shù)的返回值是一個觀察器實例。實例的observe方法可以指定觀察哪個 DOM 節(jié)點。
// 開始觀察
io.observe(document.getElementById('container'));
// 停止觀察
io.unobserve(element);
// 關閉觀察器
io.disconnect();
上面代碼中,observe的參數(shù)是一個 DOM 節(jié)點對象。
如果要觀察多個節(jié)點,就要多次調用這個方法。
io.observe(elementA);
io.observe(elementB);
代碼實現(xiàn)
看完相關的API,下面就讓我們基于IntersectionObserver來實現(xiàn)圖片懶加載:
const imgs = document.querySelectorAll('img') //獲取所有待觀察的目標元素
var options = {}
function lazyLoad(target) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entrie => {
if (entrie.isIntersecting) {
const img = entrie.target;
const src = img.getAttribute('src');
img.setAttribute('src', src)
observer.unobserve(img); // 停止監(jiān)聽已開始加載的圖片
}
})
}, options);
observer.observe(target)
}
imgs.forEach(lazyLoad)
img.loading=lazy
最后這種相對就簡單很多了,它是 Chrome 自帶的原生 lazyload 屬性。我們先來看下他在各大瀏覽器的支持程度:

“其實支持程度還不是特別好,我們你的應用對于瀏覽器兼容性要求比較高的話,建議還是先觀望一波~
它的使用也非常簡單,如標題所示:
<img src="example.jpg" loading="lazy" alt="zhangxinxu" width="250" height="150">
關于原生懶加載 loading=”lazy”的更多介紹可以參考張鑫旭大佬的瀏覽器 IMG 圖片原生懶加載 loading=”lazy”實踐指南[4]。
參考資料
MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/clientHeight
[2]阮一峰博客: http://www.ruanyifeng.com/blog/2009/09/find_element_s_position_using_javascript.html
[3]IntersectionObserver API: http://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html
[4]瀏覽器 IMG 圖片原生懶加載 loading=”lazy”實踐指南: https://www.zhangxinxu.com/wordpress/2019/09/native-img-loading-lazy/

回復“加群”與大佬們一起交流學習~
點擊“閱讀原文”查看 100+ 篇原創(chuàng)文章
