1、為什麼要懶載入或者預載入
圖片對頁面載入速度影響非常大
當頁面圖片比較多,載入速度慢,非常影響使用者體驗
思考一下,頁面有可能有幾百張圖片,但是首屏上需要展示的可能就一張而已,其他的那些圖片能不能晚一點再載入,比如使用者往下滾動的時候……
這是為什麼要用懶載入的原因
那預載入呢?
這個非常語義化,預備,提前……
就是讓使用者感覺到你載入圖片非常快,甚至使用者沒有感受到你在載入圖片
2、懶載入原理
圖片先用佔位符表示,不要將圖片地址放到src
屬性中,而是放到其它屬性(data-original
)中
頁面載入完成後,監聽視窗滾動,當圖片出現在視窗中時再給它賦予真實的圖片地址,也就是將data-original
中的屬性拿出來放到src屬性中
在滾動頁面的過程中,通過給scroll
事件繫結lazyload
函式,不斷的載入出需要的圖片
注意:請對lazyload
函式使用防抖與節流,不懂這兩的可以自己去查
3、懶載入方式
1)純粹的延遲載入,使用setTimeOut或setInterval
這種方式,本質上不算懶載入 載入完首屏內容後,隔一段時間,去載入全部內容 但這個時間差已經完成了使用者對首屏載入速度的期待
2)條件載入
使用者點選或者執行其他操作再載入
其實也包括的滾動可視區域,但大部分情況下,大家說的懶載入都是隻可視區域的圖片懶載入,所以就拿出來說了
3)可視區載入
這裡也分為兩種情況:
1、頁面滾動的時候計算圖片的位置與滾動的位置
2、通過新的API: IntersectionObserver API
(可以自動”觀察”元素是否可見)Intersection Observer API – Web API 介面 | MDN
4、懶載入程式碼實現
1、核心原理
將非首屏的圖片的src屬性設定一個預設值,監聽事件scroll
、resize
、orientationchange
,判斷元素進入視口viewport時則把真實地址賦予到src
上
2、img標籤自定義屬性相關
<img class="lazy" src="[佔點陣圖]" src="[真實url地址]" data-srcset="[不同螢幕密度下,不同的url地址]" alt="I`m an image!">
複製程式碼
如上,data-*
屬於自定義屬性, ele.dataset.*
可以讀取自定義屬性集合 img.srcset
屬性用於設定不同螢幕密度下,image自動載入不同的圖片,比如<img src="image-128.png" srcset="image-256.png 2x" />
3、判斷元素進入視口viewport
常用的方式有兩種
1)、圖片距離頂部距離 < 視窗高度 + 頁面滾動高度(太LOW了~)
imgEle.offsetTop < window.innerHeight + document.body.scrollTop
複製程式碼
2)getBoundingClientRect
(很舒服的一個API)
Element.getBoundingClientRect()
方法返回元素的大小及其相對於視口的位置,具體參考文件Element.getBoundingClientRect() – Web API 介面 | MDN
function isInViewport(ele) {
// 元素頂部 距離 視口左上角 的距離top <= 視窗高度 (反例:元素在螢幕下方的情況)
// 元素底部 距離 視口左上角 的距離bottom > 0 (反例:元素在螢幕上方的情況)
// 元素display樣式不為none
const notBelow = ele.getBoundingClientRect().top <= window.innerHeight ? true : false;
const notAbove = ele.getBoundingClientRect().bottom >= 0 ? true : false;
const visable = getComputedStyle(ele).display !== "none" ? true : false;
return notBelow && notAbove && visable ? true : false;
}
複製程式碼
3)Intersection Observer
(存在相容性問題,但帥啊)
由於相容性問題,暫時不寫,具體可參考文件 Intersection Observer – Web API 介面 | MDN
4、具體實現(demo)
核心內容都在上面分析完了,下面就是整合一下,
1)適合簡單的HTML檔案或者服務端直出的首頁
注意DOMContentLoaded
,在DOM解析完之後立馬執行,不適合前後端分離的單頁應用,因為SPA應用一般來說圖片資料是非同步請求的,在DOMContentLoaded
的時候,頁面上未必完全解析完JS和CSS,這時候let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
拿到的不是真正首屏的所有圖片標籤
document.addEventListener("DOMContentLoaded", () => {
// 獲取所有class為lazy的img標籤
let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
// 這個active是節流throttle所用的標誌位,這裡用到了閉包知識
let active = false;
const lazyLoad = () => {
// throttle相關:200ms內只會執行一次lazyLoad方法
if (active) return;
active = true;
setTimeout(() => {
lazyImages.forEach(lazyImage => {
// 判斷元素是否進入viewport
if (isInViewport(lazyImage)) {
// <img class="lazy" src="[佔點陣圖]" src="[真實url地址]" data-srcset="[不同螢幕密度下,不同的url地址]" alt="I`m an image!">
// ele.dataset.* 可以讀取自定義屬性集合,比如data-*
// img.srcset 屬性用於設定不同螢幕密度下,image自動載入不同的圖片 比如<img src="image-128.png" srcset="image-256.png 2x" />
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
// 刪除class 防止下次重複查詢到改img標籤
lazyImage.classList.remove("lazy");
}
// 更新lazyImages陣列,把還沒處理過的元素拿出來
lazyImages = lazyImages.filter(image => {
return image !== lazyImage;
});
// 當全部處理完了,移除監聽
if (lazyImages.length === 0) {
document.removeEventListener("scroll", lazyLoad);
window.removeEventListener("resize", lazyLoad);
window.removeEventListener("orientationchange", lazyLoad);
}
})
active = false;
}, 200);
}
document.addEventListener("scroll", lazyLoad);
document.addEventListener("resize", lazyLoad);
document.addEventListener("orientationchange", lazyLoad);
})
複製程式碼
2)、適合單頁應用的寫法(模擬封裝vue的懶載入)
① 核心實現
- 因為是demo,所以執行時機放到vue的全域性
mounted
鉤子裡面(這樣的首屏體驗其實是不好的),不過足夠理解就好了 - 跟上面不同的地方:l
et lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
的獲取時機放在了定時器裡面,不是一開始就拿到全域性的lazyImages
,而是每次重新整理時才拿到還沒處理過的
function LazyLoad() {
// 這個active是節流throttle所用的標誌位,這裡用到了閉包知識
let active = false;
const lazyLoad = () => {
// throttle相關:200ms內只會執行一次lazyLoad方法
if (active) return;
active = true;
setTimeout(() => {
// 獲取所有class為lazy的img標籤,這裡由於之前已經把處理過的img標籤的class刪掉了 所以不會重複查詢
let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
lazyImages.forEach(lazyImage => {
// 判斷元素是否進入viewport
if (isInViewport(lazyImage)) {
// <img class="lazy" src="[佔點陣圖]" src="[真實url地址]" data-srcset="[不同螢幕密度下,不同的url地址]" alt="I`m an image!">
// ele.dataset.* 可以讀取自定義屬性集合,比如data-*
// img.srcset 屬性用於設定不同螢幕密度下,image自動載入不同的圖片 比如<img src="image-128.png" srcset="image-256.png 2x" />
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
// 刪除class 防止下次重複查詢到改img標籤
lazyImage.classList.remove("lazy");
}
// 當全部處理完了,移除監聽
if (lazyImages.length === 0) {
document.removeEventListener("scroll", lazyLoad);
window.removeEventListener("resize", lazyLoad);
window.removeEventListener("orientationchange", lazyLoad);
}
})
active = false;
}, 200);
}
document.addEventListener("scroll", lazyLoad);
document.addEventListener("resize", lazyLoad);
document.addEventListener("orientationchange", lazyLoad);
}
複製程式碼
② 在全域性中的`mounted`鉤子中執行
const vm = new Vue({
el: `.wrap`,
store,
mounted: function () {
LazyLoad();
}
});
複製程式碼
③ 封裝 img-lazy
元件
<template>
<img :class="[`lazy`, className]" :src="defaultImg" :src="url" :data-srcset="`${url} 1x`" alt="fordeal">
</template>
<script>
export default {
props: {
url: {
type: String
},
defaultImg: {
type: String,
default: [預設圖片]
className: {
type: String,
default: ``
}
}
}
</script>
複製程式碼
④ 使用
<img-lazy className="image" :url="item.display_image" />複製程式碼
以上實現的只是比較粗糙的版本,要真正實現效能大幅提升優化還需要處理較多的細節,本文旨在讓幫助部分同學瞭解基本原理,有了巨集觀的認識後,可以嘗試去讀一下相關這種懶載入外掛的原始碼,能學到不少東西。
感謝
感謝您耐心看到這裡,希望有所收穫!
我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看amandakelake的Github部落格