原文地址:圖片懶載入踩坑
原理
對網頁載入速度影響最大的就是圖片,一張普通的圖片可能會有好幾 M 的大小,當圖片很多時,網頁的載入速度變得很緩慢。
為了優化網頁效能以及使用者體驗,我們對圖片進行懶載入
。
懶載入是一種對網頁效能優化的方式,它的原理是優先載入在可視區域內的圖片,而不一次性載入所以圖片。當瀏覽器滾動,圖片進入可視區時再去載入圖片。通過設定圖片的 src
屬性來讓瀏覽器發起圖片的請求。當這個屬性為空或者沒有時,就不會傳送請求。
實現
此文所涉及的懶載入皆是在垂直方向上的滾動載入,橫向滾動暫不考慮。
懶載入的實現主要是判斷當前圖片是否到了可視區域這一核心邏輯。我們先來整理一下實現思路:
- 拿到所有的圖片
img dom
。 - 遍歷每個圖片判斷當前圖片是否到了可視區範圍內。
- 如果到了就設定圖片的 src 屬性。
- 繫結 window 的
scroll
事件,對其進行事件監聽。
HTML 結構
<div class="container">
<div class="img-area">
<img id="first" data-src="./img/ceng.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/data.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/huaji.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/liqi1.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/liqi2.png" alt="">
</div>
<div class="img-area">
<img data-src="./img/steve-jobs.jpg" alt="">
</div>
</div>
複製程式碼
此時 img 標籤是沒有 src 屬性的,我們把真實的圖片地址放在一個屬性裡,這裡我們使用 HTML5 的 data
屬性,將真實地址放在自定義的 data-src
中。
判斷圖片是否進入了可視區域
這一邏輯有兩種方法,聽我娓娓道來。
方法一
第一種方法我們通過計算該圖片距離 document
頂部的高度是否小於當前可視區域相對於 document 頂部高度來判斷。
可視區域相對於 document 頂部高度的計算方法:
const clientHeight = document.documentElement.clientHeight; // 視口高度,也就是視窗的高度。
const scrollHeight = document.documentElement.scrollTop + clientHeight; // 滾動條偏移文件頂部的高度(也就是文件從頂部開始到可視區域被抹去的高度) + 視口高度
複製程式碼
畫了一張圖方便理解:
然後就是計算該圖片距離文件頂部的高度。有兩種方法,第一種方法是通過元素的 offsetTop
屬性來計算。從上圖我們瞭解到元素的 offsetTop 屬性是相對於一個 position
為 非 static
的祖先元素,也就是 child.offsetParent
。同時需要將祖先元素的 border
考慮在內,我們通過child.offsetParent.clientTop
可以拿到邊框厚度。
由此我們得到元素距離文件頂部的高度的計算方法:
function getTop(el, initVal) {
let top = el.offsetTop + initVal;
if (el.offsetParent !== null) {
top += el.offsetParent.clientTop;
return getTop(el.offsetParent, top);
} else {
return top;
}
}
複製程式碼
這裡的這個方法使用了 尾遞迴呼叫
。可以提高遞迴效能。當然這裡也可以用迴圈來實現:
function getTop(el) {
let top = el.offsetTop;
var parent = el.offsetParent;
while(parent !== null) {
top += parent.offsetTop + parent.clientTop;
parent = parent.offsetParent;
}
return top;
}
複製程式碼
第二種方法是使用 element.getBoundingClientRect()
API 直接得到 top 值。
getBoundingClientRect 的返回值如下圖:
var first = document.getElementById('first');
getTop(first, 0); // 130
console.log(first.getBoundingClientRect().top); // 130
複製程式碼
於是我們得到判斷方法:
function inSight(el) {
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollTop + clientHeight;
// 方法一
return getTop(el, 0) < scrollHeight;
// 方法二
// return el.getBoundingClientRect().top < clientHeight;
}
複製程式碼
接下來就是對每個圖片進行判斷和賦值。
function loadImg(el) {
if (!el.src) {
el.src = el.dataset.src;
}
}
function checkImgs() {
const imgs = document.getElementsByTagName('img');
Array.from(imgs).forEach(el => {
if (inSight(el)) {
loadImg(el);
}
})
console.log(count++);
}
複製程式碼
最後給 window 繫結 onscroll
事件以及 onload
事件:
window.addEventListener('scroll', checkImgs, false);
window.onload = checkImgs;
複製程式碼
我們知道類似 scroll
或 resize
這樣的事件瀏覽器可能在很短的時間內觸發很多次,為了提高網頁效能,我們需要一個節流函式來控制函式的多次觸發,在一段時間內(如 500ms)只執行一次回撥。
/**
* 持續觸發事件,每隔一段時間,只執行一次事件。
* @param fun 要執行的函式
* @param delay 延遲時間
* @param time 在 time 時間內必須執行一次
*/
function throttle(fun, delay, time) {
var timeout;
var previous = +new Date();
return function () {
var now = +new Date();
var context = this;
var args = arguments;
clearTimeout(timeout);
if (now - previous >= time) {
fun.apply(context, args);
previous = now;
} else {
timeout = setTimeout(function () {
fun.apply(context, args);
}, delay);
}
}
}
window.addEventListener('scroll', throttle(checkImgs, 200, 1000), false);
複製程式碼
方法二
HTML5 有一個新的 IntersectionObserver
API,它可以自動觀察元素是否可見。
主要用法:
var observer = new IntersectionObserver(callback, option);
// 開始觀察
observer.observe(document.getElementById('first'));
// 停止觀察
observer.unobserve(document.getElementById('first'));
// 關閉觀察器
observer.disconnect();
複製程式碼
目標的可見性發生變化時就會呼叫觀察器的 callback。
function callback(changes: IntersectionObserverEntry[]) {
console.log(changes[0])
}
// IntersectionObserverEntry
{
time: 29.499999713152647,
intersectionRatio: 1,
boundingClientRect: DOMRectReadOnly {
bottom: 144,
height: 4,
left: 289,
right: 293,
top: 140,
width: 4,
x: 289,
y: 140
},
intersectionRect: DOMRectReadOnly,
isIntersecting: true,
rootBounds: DOMRectReadOnly,
target: img#first
}
複製程式碼
詳細釋義:
- time: 可見性發生變化的時間,是一個高精度時間戳,單位為毫秒
- intersectionRatio: 目標元素的可見比例,即 intersectionRect 佔 boundingClientRect 的比例,完全可見時為 1 ,完全不可見時小於等於 0
- boundingClientRect: 目標元素的矩形區域的資訊
- intersectionRect: 目標元素與視口(或根元素)的交叉區域的資訊
- rootBounds: 根元素的矩形區域的資訊,getBoundingClientRect() 方法的返回值,如果沒有根元素(即直接相對於視口滾動),則返回 null
- isIntersecting: 是否進入了視口,boolean 值
- target: 被觀察的目標元素,是一個 DOM 節點物件
使用 IntersectionObserver 實現圖片懶載入:
function query(tag) {
return Array.from(document.getElementsByTagName(tag));
}
var observer = new IntersectionObserver(
(changes) => {
changes.forEach((change) => {
if (change.intersectionRatio > 0) {
var img = change.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
})
}
)
query('img').forEach((item) => {
observer.observe(item);
})
複製程式碼
完整程式碼見 github
完 :)