圖像延遲加載
想要得到更好的性能體驗(yàn),只靠資源壓縮與恰當(dāng)?shù)奈募袷竭x型,是很難滿足期望的。我們還需要針對(duì)資源加載過程進(jìn)行優(yōu)化。
什么是延遲加載?
下圖是京東商城的手機(jī)端首頁,當(dāng)元素沒有滑動(dòng)到視線內(nèi)時(shí),圖片src屬性放置了一個(gè)很小的圖片,init_src屬性放置了真正的圖片,只要當(dāng)該元素滑動(dòng)到視線內(nèi)部,才會(huì)將init_src屬性賦值給src去加載真實(shí)的圖片,這就是一個(gè)簡(jiǎn)單的圖片延遲加載的過程。
傳統(tǒng)方式延遲加載
就是事件監(jiān)聽的方式,通過監(jiān)聽scroll事件與resize事件,并在事件的回調(diào)函數(shù)中去判斷,需要進(jìn)行延遲加載的圖片是否進(jìn)入視窗區(qū)域。
我們只需要關(guān)注三個(gè)屬性。
- class屬性,稍后會(huì)在JAVAScript中使用類選擇器選取需要延遲加載處理的〈img〉標(biāo)簽。
- src屬性,加載前的占位符圖片,可用Base64圖片或低分辨率的圖片。
- data-src屬性,通過該自定義屬性保存圖片真實(shí)的URL外鏈。
加入頁面中有多張這樣的圖片需要加載。具體的JavaScript實(shí)現(xiàn)邏輯如下,在文檔的DOMContentLoaded事件中,添加延遲加載處理邏輯,首先獲取class屬性名為lazy的所有〈img〉標(biāo)簽,將這些標(biāo)簽暫存在一個(gè)名為lazyImages的數(shù)組中,表示需要進(jìn)行延遲加載但還未加載的圖片集合。當(dāng)一個(gè)圖片被加載后,便將其從lazyImages數(shù)組中移除,直到lazyImages數(shù)組為空時(shí),表示所有待延遲加載的圖片均已經(jīng)加載完成,此時(shí)便可將頁面滾動(dòng)事件移除。
這里使用了getBoundingClientRect()函數(shù)獲取元素的相對(duì)位置.
rectObject = object.getBoundingClientRect();
rectObject.top:元素上邊到視窗上邊的距離;
rectObject.right:元素右邊到視窗左邊的距離;
rectObject.bottom:元素下邊到視窗上邊的距離;
rectObject.left:元素左邊到視窗左邊的距離;
對(duì)于只可上下滾動(dòng)的頁面,判斷一個(gè)圖片元素是否出現(xiàn)在屏幕視窗中的方法其實(shí)顯而易見,即當(dāng)元素上邊緣距屏幕視窗頂部的top值小于整個(gè)視窗的高度window.innerHeight時(shí),預(yù)加載的事件處理代碼如下:
document.addEventListener(DOMContentLoaded, function() {
const imags = [].slice.call(document.querySelector('.lazy'))
const active = false; // 限制函數(shù)被頻繁調(diào)動(dòng)
function load() {
if(active === false) {
active = true
setTimeout(() => {
imags.forEach((img) => {
const objPos = img.getBoundingClientRect();
if(objPos.top <= window.innerHeight && objPos.bottom >=0 && img.display !== 'done') {
img.src = img.dataset.src;
img.classList.remove('lazy')
imags.filter((i) => (i !== img))
if(imags.length === 0) {
document.removeEventListener('scroll', load)
window.removeEventListener('resize', load)
window.removeEventListener('orientationchange', load)
}
}
})
active = false
}, 200)
}
}
document.addEventListener('scroll', load)
window.addEventListener('resize', load)
window.addEventListener('orientationchange', load)
})
這種方式的有點(diǎn)就是兼容性比較好,缺點(diǎn)是頻繁地進(jìn)行計(jì)算必然會(huì)影響性能,代碼也會(huì)比較繁瑣。
實(shí)現(xiàn)圖片的延遲加載:Intersection Observer方式
現(xiàn)代瀏覽器已大多支持了Intersection Observer API,用一句話簡(jiǎn)述:每當(dāng)因頁面滾動(dòng)或窗口尺寸發(fā)生變化,使得目標(biāo)元素(target)與設(shè)備視窗或其他指定元素產(chǎn)生交集時(shí),便會(huì)觸發(fā)通過Intersection Observer API配置的回調(diào)函數(shù),在該回調(diào)函數(shù)中進(jìn)行延遲加載的邏輯處理,會(huì)比傳統(tǒng)方式顯得更加簡(jiǎn)潔而高效。
簡(jiǎn)單來說,目標(biāo)元素的可見性變化時(shí),就會(huì)調(diào)用觀察器的回調(diào)函數(shù) callback。
callback一般會(huì)觸發(fā)兩次。一次是目標(biāo)元素剛剛進(jìn)入視口(開始可見),另一次是完全離開視口(開始不可見)。
document.addEventListener(DOMContentLoaded, function() {
const imags = [].slice.call(document.querySelector('.lazy'))
if(window.IntersectionObserver && window.IntersectionObserverEntry && window.IntersectionObserverEntry.prototype.intersectionRatio) {
var lazyImgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry)=> {
if(entry.isIntersecting) {
var lazyImg = entry.target;
lazyImg.src = lazyImg.dataset.src;
lazyImg.classList.remove('lazy');
lazyImgObserver.unobserve(lazyImg)
}
})
})
imags.forEach((img) => {
lazyImgObserver.observe(img)
})
}
})
這種方式判斷元素是否出現(xiàn)在視窗中更為簡(jiǎn)單直觀,應(yīng)在實(shí)際開發(fā)中盡量使用,但其問題是并非所有瀏覽器都能兼容。
(1)做好盡量完備瀏覽器兼容性檢查,對(duì)于兼容Intersection Observer API的瀏覽器,采用這種方式進(jìn)行處理,而對(duì)于不兼容的瀏覽器,則切換回傳統(tǒng)的實(shí)現(xiàn)方式進(jìn)行處理。 (2)使用相應(yīng)兼容的polyfill插件,在W3C官方GitHub賬號(hào)下就有提供。
實(shí)現(xiàn)圖片的延遲加載:css類名方式
這種實(shí)現(xiàn)方式通過CSS的background-image屬性來加載圖片,與判斷〈img〉標(biāo)簽src屬性是否有要請(qǐng)求圖片的URL不同,CSS中圖片加載的行為建立在瀏覽器對(duì)文檔分析基礎(chǔ)之上。
document.addEventListener(DOMContentLoaded, function() {
const imags = [].slice.call(document.querySelector('.lazy'))
if(window.IntersectionObserver && window.IntersectionObserverEntry && window.IntersectionObserverEntry.prototype.intersectionRatio) {
var lazyImgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry)=> {
if(entry.isIntersecting) {
var lazyImg = entry.target;
lazyImg.classList.add('visible');
lazyImgObserver.unobserve(lazyImg)
}
})
})
imags.forEach((img) => {
lazyImgObserver.observe(img)
})
}
})
這種方式限制于需要提前寫好css樣式。
原生的延遲加載支持
除了上述通過開發(fā)者手動(dòng)實(shí)現(xiàn)延遲加載邏輯的方式,從Chrome 75版本開始,已經(jīng)可以通過〈img〉和〈iframe〉標(biāo)簽的loading屬性原生支持延遲加載了,loading屬性包含以下三種取值。
● lazy:進(jìn)行延遲加載。 ● eager:立即加載。 ● auto:瀏覽器自行決定是否進(jìn)行延遲加載。
測(cè)試:image標(biāo)簽就是 img
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- <script src="js/file2.js"></script> -->
<!-- <script src="js/file3.js"></script> -->
<!-- <link rel="stylesheet" href="css/index.css"> -->
<style>
img {
width: 700px;
height: 200px;
display: block;
}
</style>
</head>
<body>
<imgage loading="lazy" src='./image/home-1.png' alt="photo" />
<imgage loading="lazy" src='./image/home-2.png' alt="photo" />
<imgage loading="lazy" src='./image/home-3.png' alt="photo" />
<imgage loading="lazy" src='./image/home-4.png' alt="photo" />
<imgage loading="lazy" src='./image/home-5.png' alt="photo" />
<imgage loading="lazy" src='./image/home-6.png' alt="photo" />
<imgage loading="lazy" src='./image/home-7.png' alt="photo" />
<imgage loading="lazy" src='./image/home-8.png' alt="photo" />
<imgage loading="lazy" src='./image/home-9.png' alt="photo" />
<imgage loading="lazy" src='./image/home-10.png' alt="photo" />
<imgage loading="lazy" src='./image/home-11.png' alt="photo" />
<imgage loading="lazy" src='./image/home-12.png' alt="photo" />
<imgage loading="lazy" src='./image/home-13.png' alt="photo" />
<imgage loading="lazy" src='./image/home-14.png' alt="photo" />
<imgage loading="lazy" src='./image/home-15.png' alt="photo" />
<imgage loading="lazy" src='./image/home-16.png' alt="photo" />
<imgage loading="lazy" src='./image/home-17.png' alt="photo" />
<imgage loading="lazy" src='./image/home-18.png' alt="photo" />
<imgage loading="lazy" src='./image/home-19.png' alt="photo" />
<imgage loading="lazy" src='./image/home-20.png' alt="photo" />
<imgage loading="lazy" src='./image/home-21.png' alt="photo" />
<imgage loading="lazy" src='./image/home-22.png' alt="photo" />
<imgage loading="lazy" src='./image/home-23.png' alt="photo" />
<imgage loading="lazy" src='./image/home-24.png' alt="photo" />
<imgage loading="lazy" src='./image/home-25.png' alt="photo" />
<imgage loading="lazy" src='./image/home-26.png' alt="photo" />
<imgage loading="lazy" src='./image/home-27.png' alt="photo" />
<imgage loading="lazy" src='./image/home-28.png' alt="photo" />
<imgage loading="lazy" src='./image/home-29.png' alt="photo" />
<imgage loading="lazy" src='./image/home-30.png' alt="photo" />
</body>
</html>
可以看到,首次加載的個(gè)數(shù)是13個(gè),首屏一般只能放下4個(gè)左右,13個(gè)以后的img滾動(dòng)到視線內(nèi)部會(huì)自動(dòng)去加載。
實(shí)踐發(fā)現(xiàn)有以下幾個(gè)特點(diǎn):
- Lazy loading加載數(shù)量與屏幕高度有關(guān),高度越小加載數(shù)量越少,但并不是線性關(guān)系。
- Lazy loading加載數(shù)量與網(wǎng)速有關(guān),網(wǎng)速越慢,加載數(shù)量越多,但并不是線性關(guān)系。
- Lazy loading加載沒有緩沖,滾動(dòng)即會(huì)觸發(fā)新的圖片資源加載。
- Lazy loading加載在窗口resize尺寸變化時(shí)候也會(huì)觸發(fā),例如屏幕高度從小變大的時(shí)候。
- Lazy loading加載也有可能會(huì)先加載后面的圖片資源,例如頁面加載時(shí)滾動(dòng)高度很高的時(shí)候。
與JavaScript有關(guān)的幾個(gè)行為特征:
- 判斷瀏覽器是否支持原生loading,最好使用'loading' in XXX判斷。
- 獲取loading屬性值可以直接img.loading;
- 原生loading不可寫,不可訪問例如HTMLImageElement.prototype.loading會(huì)報(bào)錯(cuò)Illegal invocation。
- 如果要使用,注意做兼容性處理。