圖片延時載入十分重要,尤其是對於移動端使用者。
從理論上來看,影像延遲載入機制十分簡單,但實際上卻有很多需要注意的細節。 此外,有多個不同的用例均受益於延遲載入。 首先,我們來了解一下在 HTML 中延遲載入內聯影像。
延遲載入是一種在載入頁面時,延遲載入非關鍵資源的方法, 而這些非關鍵資源則在需要時才進行載入。 就影像而言,“非關鍵”通常是指“螢幕外”。
最近在做一個移動端漫畫應用(id.mangaya.mobi),涉及的圖片比較多,如果圖片不做額外處理,會對使用者不太友好,而且lighthouse評分也會因此降低。
場景
主要有兩種場景。
有興趣的同學可以檢視react-progressive-lazy-image,使用起來非常簡單!
Senario 1: 圖片延時到在視窗viewport內才開始載入。
Senario 2: 使用者觀看漫畫,需要漫畫一張張的順序載入。
原理
圖片懶載入技術主要通過監聽圖片資源容器是否出現在視口區域內,來決定圖片資源是否被載入。
那麼實現圖片懶載入技術的核心就是如何判斷元素處於視口區域之內。
實踐
那麼如何實現呢?
getBoundingClientRect()
返回的值是一個DOMRect物件,它是getClientRects()元素返回的矩形的並集,即與元素關聯的CSS邊框。其結果是,其包含整個元件,具有隻讀的最小矩形left,top,right,bottom,x,y,width,和height性質描述在畫素整體邊界框。除視口左上角width和height相對於視口左上角的屬性。
下面程式碼可以判斷元素是否在視窗內//https://gomakethings.com/how-to-test-if-an-element-is-in-the-viewport-with-vanilla-javascript/
export const elementIsInsideViewport = el => {
const bounding = el.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <=
(window.innerWidth || document.documentElement.clientWidth)
);
};
複製程式碼
可以結合window.onScroll以及window.onResize事件以及throttle來實現對img元素的判斷。
document.addEventListener("DOMContentLoaded", function() {
let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
let active = false;
const lazyLoad = function() {
if (active === false) {
active = true;
setTimeout(function() {
lazyImages.forEach(function(lazyImage) {
if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
lazyImage.classList.remove("lazy");
lazyImages = lazyImages.filter(function(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);
window.addEventListener("resize", lazyLoad);
window.addEventListener("orientationchange", lazyLoad);
});
複製程式碼
此程式碼在 scroll 事件處理程式中使用 getBoundingClientRect 來檢查是否有任何 img.lazy 元素處於視口中。 使用 setTimeout 呼叫來延遲處理,active 變數則包含處理狀態,用於限制函式呼叫。 延遲載入影像時,這些元素隨即從元素陣列中移除。 當元素陣列的 length 達到 0 時,滾動事件處理程式程式碼隨即移除。
雖然此程式碼幾乎可在任何瀏覽器中正常執行,但卻存在潛在的效能問題,即重複的 setTimeout 呼叫可能純屬浪費,即使其中的程式碼受限制,它們仍會執行。 在此示例中,當文件滾動或視窗調整大小時,不管視口中是否有影像,每 200 毫秒都會執行一次檢查。 此外,跟蹤尚未延遲載入的元素數量,以及取消繫結滾動事件處理程式的繁瑣工作將由開發者來完成。
最佳實踐 使用 Intersection Observer
此方法非常簡單,只需要為元素生成一個IntersectionObserver,並且監聽該元素,然後在監聽的回撥判斷元素的intersectionRatio比率即可達到所需。這是核心程式碼.
componentDidMount() {
const { src, needLazyUtilInViewPort, canLoadRightNow } = this.props;
if (needLazyUtilInViewPort) {
//https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
try {
const node = ReactDOM.findDOMNode(this);
this.observer = new IntersectionObserver(this.insideViewportCb);
this.observer.observe(node);
} catch (err) {
console.log("err in finding node", err);
}
} else {
if (canLoadRightNow) {
this.loadImage(src);
}
}
}
insideViewportCb(entries) {
entries.forEach(element => {
//在viewport裡面
if (element.intersectionRatio >0) {
this.loadImage(this.props.src);
}
});
}
複製程式碼
不過,Intersection Observer 的缺點是雖然在瀏覽器之間獲得良好的支援,但並非所有瀏覽器皆提供支援。 對於不支援 Intersection Observer 的瀏覽器,您可以使用 polyfill,或者如以上程式碼所述,檢測 Intersection Observer 是否可用,並在其不可用時回退到相容性更好的舊方法。
至於列表圖順序載入的話,只需要在每個圖片回撥通知父元件可以載入下一張就可以了。 總的來說getBoundingClientRect和Intersection Observer都可以實現圖片懶載入,但是getBoundingClientRect如果在當前頁面使用到其他onScroll事件,會出現卡頓等問題,不能非常順暢的滑動,而Intersection Observer使用起來非常簡單流暢。
影像延遲載入 && 列表圖順序載入的元件已經開源啦~!
有興趣的同學可以檢視react-progressive-lazy-image,使用起來非常簡單!