圖片和視訊的懶載入
基於 ofollow,noindex" target="_blank">Lazy Loading Images and Video 一文翻譯,略有刪減
對於一個網站的有效載荷而言, 圖片 和 視訊 是非常重要的一部分內容。然而,專案的利益相關者並不願意從已有的應用中削減媒體資源,當專案所有相關人想提高網站效能但卻無法就如何實現目標達成一致時,這種障礙是非常令人沮喪的。幸運的是,懶載入(Lazy Loading)是一種可以降低初始頁面負載和載入時間的解決方案,但它並不會減少內容的載入。
什麼是懶載入
懶載入是一種在頁面載入時,延遲載入非關鍵資源的技術,而這些非關鍵資源會在需要的時候才會被載入。對於圖片而言,"非關鍵"資源即"螢幕外"(off-screen)的資源。如果你使用了 Lighthouse 並檢查其提供的一些改善機會,就有可能從 Offscreen Images audit 方面看到一些指導:
Lighthouse 進行效能審計的一個方面就是確定螢幕外的圖片資源(off screen images),這些圖片資源都可以用於懶載入。
或許你已經在某些網站中看到到懶載入的實踐,其過程大概如下:
- 你訪問一個頁面,並滾動頁面去閱讀內容
- 在某一時刻,一個佔位的圖片滾動到了視口中
- 這個佔位的圖片裡面被最終的真實圖片替換了
你可以在流行的內容釋出平臺 Medium 上找到圖片懶載入的例子。Medium 在頁面載入的時候是先載入輕量地佔點陣圖片,當滾動到該區域時,佔位圖片會被懶載入完成的圖片替換。
左邊是頁面載入時顯示的佔位圖片,右圖是懶載入完成之後的真實圖片
如果你不熟悉懶載入,那你可能想知道這樣的技術是有多有用,以及使用它的優點是什麼。接著往下讀並找出你要的答案。
為什麼要對圖片或視訊進行懶載入而不是直接載入?
因為直接載入可能會載入一些使用者從不會看的資源,這就會導致一些問題:
- 會浪費(使用者的)流量。如果網路連線是不計流量的,這還不算是很糟糕的事情,但如果流量是有限制的,載入使用者不會看到的資源則實際上是在浪費(使用者的)錢
- 會延長處理時間、浪費裝置的電量以及其它系統資源。媒體(視訊或圖片)資源被載入之後,瀏覽器必須對其進行解碼併網頁內渲染媒體資源的內容
當我們對圖片和視訊進行懶載入,這不僅會減少初始頁面的載入時間、大小以及系統資源的佔用,還會對應用效能的提升產生積極影響。在這篇文章中,我們會針對圖片和視訊的懶載入實現提及一些技術和理論指導,也會列舉一些常用的與懶載入相關的庫。
圖片的懶載入
從理論上來說,圖片的懶載入機制是非常簡單的,但實現上卻有很多需要注意的細節。加上有幾個不同的用例都可以受益於懶載入,因而讓我們先從內聯圖片的懶載入開始。
內聯圖片
圖片最常用的懶載入方式是使用 <img>
元素。當對 <img>
元素進行懶載入時,我們會通過 JavaScript/">JavaScript 來判斷 <img>
元素是否在視口內,如果元素在視口內,則它的 src
屬性(有時也用 srcset
)會用 URLs 填充來載入所需的影象內容。
使用 intersection observer
如果你之前寫過實現懶載入的程式碼,你可能是使用事件處理的方式來判斷 <img>
元素是否可見,進而實現懶載入,如 scroll
或 resize
。儘管這種方式有更好的瀏覽器相容性,但現代瀏覽器通過 intersection observer API 提供了一個更完美且更高效的方式來判斷元素是否可見。
注意:並非所有的現代瀏覽器支援 intersection observer,如果想要更好的瀏覽器相容性,可以參考下一小節,它將告訴你怎麼通過 scroll 事件和 resize 事件實現圖片的懶載入。
相對於程式碼依賴各種各樣的事件處理,intersection observer 的易用性和可讀性更強,因為開發者只需要註冊一個觀察者(observer)來監聽元素即可,而不用寫非常冗長的程式碼來檢測元素是否可見。註冊觀察者之後,之後需要開發者做的就是決定當元素可見時需要做什麼。
假設我們需要懶載入的 <img>
元素的基礎程式碼如下:
<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">
對於上述程式碼,我們應該關注程式碼的三個相關部分:
-
class
屬性,這是我們在 JavaScript 選擇元素時需要用到的類選擇器 -
src
屬性,當頁面初次載入時,它會載入一張佔位圖片 -
data-src
和data-srcset
屬性,這兩個是佔位屬性,其值是<img>
元素出現在視口內時所需要載入的圖片的 URL,且僅載入一次
現在看一下怎麼在 JavaScript 中使用 intersection observer 實現圖片的懶載入:
document.addEventListener("DOMContentLoaded", function() { var lazyImages = [].slice.call(document.querySelectorAll("img.lazy")); if ("IntersectionObserver" in window) { let lazyImageObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { let lazyImage = entry.target; lazyImage.src = lazyImage.dataset.src; lazyImage.srcset = lazyImage.dataset.srcset; lazyImage.classList.remove("lazy"); lazyImageObserver.unobserve(lazyImage); } }); }); lazyImages.forEach(function(lazyImage) { lazyImageObserver.observe(lazyImage); }); } else { // Possibly fall back to a more compatible method here } });
當 DOMContentLoaded
事件觸發時,指令碼會去查詢所有類屬性值是 lazy
的 <img>
元素。如果瀏覽器支援 intersection observer,我們就會建立一個觀察者,並且當 img.lazy
元素進入視口時執行 callback。可以在 Codepen 上檢視這段程式碼的 示例 。
注意:上述程式碼利用了 intersection observer 的 isIntersecting
方法,其在 Edge 15 的 intersection observer 實現中是不可用的,因此,上述程式碼不能執行在 Edge 15 上。參考這個 Github issue 以獲得關於更完整的特徵檢測條件的指導。
儘管現代瀏覽器對 intersection observer 有 良好的支援 ,但也並非所有瀏覽器均提供了支援。對於不支援 intersection observer 的瀏覽器,你可以使用 polyfill ,或者遵照上面程式碼的建議,先進行特徵檢測,如果不支援,則提供 fall back 方法進行相容處理。
使用事件處理(相容性更好的方式)
雖然你應該使用 intersection observer 實現懶載入,但如果你的應用對瀏覽器的相容性比較嚴格。對於不支援 intersection observer 的瀏覽器,你可以通過 polyfill 來支援,當然也可以使用 scroll 、 resize 和 orientationchange 事件以及 getBoundingClientRect 來判斷元素是否在視口內。
假設需要懶載入的 <img>
元素的基礎程式碼和上面一樣,則下面的 JavaScripts 程式碼提供了懶載入的功能:
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
變數用於節流控制。當圖片被懶載入之後,該元素會從元素的陣列中移除,而當改陣列的長度達到0,滾動事件處理器也會被移除。相關 示例程式碼 可在 CodePen 上檢視。
儘管上述程式碼的瀏覽器相容性看起來很好,但存在一個潛在的效能問題,因為 setTimeout
的呼叫可能被浪費了,即使程式碼內部作了節流處理。在這個示例中,當文件滾動或視窗調整大小時,不管是否有圖片存在視口之內,每 200ms 都會執行一次檢查。此外,對於開發者還有一項冗餘的工作:追蹤還有多少元素需要進行懶載入以及解綁滾動事件。
總之,優先使用 intersection observer,如果應用對瀏覽器的相容性比較嚴格,則降級到事件處理。
CSS 中的圖片
儘管在網頁中, img
標籤是使用圖片最常見的方式,但圖片也可以用於 CSS 的 background-image
屬性(或其它屬性)。不管 img
元素是否可見,瀏覽器均會去載入,但 CSS 中的影象載入行為有更多的推測。當 CSS 物件模型 和 渲染樹 構建完成時,在請求額外的資源之前,瀏覽器會檢查 CSS 是怎麼作用於文件的。如果瀏覽器斷定某個 CSS 規則包含額外的資源,但在當前構建時並不作用於文件,瀏覽器是不會去請求該資源的。
瀏覽器的這種推測行為可用於 CSS 中圖片懶載入,但首先需要通過 JavaScript 來判斷元素是否在視口之內,然後給該元素新增一個包含背景圖片的類名樣式,這樣圖片就會在需要的時候載入,而不是文件初始載入的時候。例如,讓一個元素包含一張非常大的英雄背景圖:
<div class="lazy-background"> <h1>Here's a hero heading to get your attention!</h1> <p>Here's hero copy to convince you to buy a thing!</p> <a href="/buy-a-thing">Buy a thing!</a> </div>
div.lazy-background
元素將通過關聯的 CSS 包含一張英雄背景圖。在這個示例中,我們會通過 visible
類名隔離 div.lazy-background
元素的 background-image
屬性,當元素進入視口之後,會給元素新增類名 visible
:
.lazy-background { background-image: url("hero-placeholder.jpg"); /* Placeholder image */ } .lazy-background.visible { background-image: url("hero.jpg"); /* The final image */ }
我們需要通過 JavaScript 來檢測元素是否在視口之內(使用 intersection observer),當元素出現在視口之內時,會給 div.lazy-background
元素新增 類名 visible
去載入真實的圖片:
document.addEventListener("DOMContentLoaded", function() { var lazyBackgrounds = [].slice.call(document.querySelectorAll(".lazy-background")); if ("IntersectionObserver" in window) { let lazyBackgroundObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { entry.target.classList.add("visible"); lazyBackgroundObserver.unobserve(entry.target); } }); }); lazyBackgrounds.forEach(function(lazyBackground) { lazyBackgroundObserver.observe(lazyBackground); }); } });
如上文所提到的,並不是所有的瀏覽器均支援 intersection observer,因為你需要為它提供一個回退方案或 polyfill。相關 示例程式碼 可在 CodePen 上檢視。
視訊的懶載入
和圖片元素一樣,我們也可以對視訊進行懶載入。正常情況下載入視訊時是通過 video
元素(也可以使用 <img>
,但功能有限)。怎麼對視訊進行懶載入取決於使用場景,接下來我們會討論兩種場景下的實現方案。
視訊不自動播放
第一種場景是視訊的播放是有使用者控制的(即視訊不自動播放),指定 <video>
元素的 HTML/Element/video#attr-preload" rel="nofollow,noindex" target="_blank"> preload
屬性 即可:
<video controls preload="none" poster="one-does-not-simply-placeholder.jpg"> <source src="one-does-not-simply.webm" type="video/webm"> <source src="one-does-not-simply.mp4" type="video/mp4"> </video>
將 <video>
元素的 preload
屬性設定為 none
可以阻止瀏覽器預載入任何視訊資料。為佔據空間,我們通過 poster
屬性給 <video>
元素一個佔位圖片,這樣做的理由是因為不同的瀏覽器對視訊的載入行為是有差異的:
- 在 Chrome 中,
preload
屬性的預設值是auto
,但從 Chrome 64 開始,其預設值是metadata
。而在 Chrome 的桌面版本中,部分視訊的預載入通過Content-Range
頭來實現的,Firefox、Edge 和 IE11 也採取類似的行為。 - 對於桌面版本的 Chrome,Safari 11 會預載入一部分視訊資料(preload a range of the video),而 Safari 11.2(當前的技術預覽版本)只會預載入視訊的元資料。 HTML5_Audio_Video/AudioandVideoTagBasics/AudioandVideoTagBasics.html#//apple_ref/doc/uid/TP40009523-CH2-SW9" rel="nofollow,noindex" target="_blank">對於 iOS Safari,則不會預載入任何視訊資料 。
- 當瀏覽器支援 資料保護模式 (Data Saver Mode),
preload
屬性的預設是none
因為瀏覽器的預設行為對預載入(preload)並不是固定不變的,因為顯示說明可能是最好的方式。在使用者控制播放的情況下,使用 preload="none"
是跨平臺實現視訊懶載入最容易的方式。但 preload
屬性並不是唯一實現視訊懶載入的方式, 通過視訊預載入實現快速播放 一文或許會給你提供一些 ideas,並會深入瞭解 JavaScript 實現視訊播放的方式。
壞訊息是,這並沒有證明用視訊代替 GIFs 動畫是有用的,下一節我們將討論這個問題。
視訊替代 GIF 動畫
儘管 GIFs 動畫應用廣泛,但它們在很多方面的表現均不如視訊,尤其是輸出檔案大小方面。GIFs 動畫可以擴充套件到幾兆位元組的資料範圍,而視覺質量相似的視訊往往要小得多。
使用 <video>
元素代替 GIF 動畫並不像 <img>
元素那麼簡單,因為 GIF 動畫本身有固定的三種行為:
- 載入時自動播放
- 會不斷迴圈播放( 但並不是始終如此 )
- 沒有音訊軌道
用 <video>
元素實現上述三種行為,程式碼可能如下:
<video autoplay muted loop playsinline> <source src="one-does-not-simply.webm" type="video/webm"> <source src="one-does-not-simply.mp4" type="video/mp4"> </video>
autoplay
、 muted
以及 loop
等屬性是 <video>
元素的自身屬性, playsinline
屬性對於在 iOS 上實現自動播放是必須的 。現在我們有了一個可用且跨平臺的視訊替代 GIFs 的方式,但是怎麼實現懶載入呢? Chrome 會主動為你對視訊進行懶載入 ,但你並不能指望所有的瀏覽器都提供了這種優化。根據你的使用者和應用對瀏覽器相容性的要求,你可能需要自己動手實現視訊的懶載入。在開始之前,先修改你的 <video>
標籤:
<video autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg"> <source data-src="one-does-not-simply.webm" type="video/webm"> <source data-src="one-does-not-simply.mp4" type="video/mp4"> </video>
你會注意到添加了 poster
屬性 ,直到視訊被載入之前,它允許你指定一個佔位圖片佔據 <video>
元素的空間。和之前實現 <img>
元素懶載入一樣,我們通過每個 <source>
元素的 data-src
屬性儲存視訊的 URL,然後使用和之前基於 intersection observer 對圖片懶載入示例類似的 JavaScript 程式碼:
document.addEventListener("DOMContentLoaded", function() { var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy")); if ("IntersectionObserver" in window) { var lazyVideoObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(video) { if (video.isIntersecting) { for (var source in video.target.children) { var videoSource = video.target.children[source]; if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") { videoSource.src = videoSource.dataset.src; } } video.target.load(); video.target.classList.remove("lazy"); lazyVideoObserver.unobserve(video.target); } }); }); lazyVideos.forEach(function(lazyVideo) { lazyVideoObserver.observe(lazyVideo); }); } });
當懶載入一個 <video>
元素時,我們需要遍歷所有的 <source>
子元素,並將 data-src
屬性的值賦予給 src
屬性,然後需要呼叫元素的 load
方法觸發視訊的載入,在視訊載入完成之後就會開始自動播放。
使用這種方法,我們就有一個視訊解決方案來模擬 GIF 動畫的行為,但卻不需要載入 GIFs 動畫那樣密集的資料,並且我們還實現了視訊內容的懶載入。
一些懶載入庫
如果你僅是想找一個支援懶載入功能的庫而並不關心懶載入的實現細節,那麼社群中有非常多的選擇。大部分的庫都會使用一個和本文 demo 中類似的標記模式(markup pattern),在這也推薦一些你可能覺得非常有用的庫:
- lazysizes 是一個功能豐富的庫,支援圖片和 iframes。它使用的標記模式和本文中的示例程式碼類似,會自動繫結到帶
lazyload
類選擇器的<img>
元素上,並需要你通過data-src
屬性或data-secret
屬性指定圖片的 URLs。該庫是基於 intersection observer (可以使用 polyfill)實現懶載入的,並有 大量的 擴充套件外掛,如 lazy load video - lozad.js 是一個非常輕量的且僅基於 intersection observer 的懶載入庫。因此,它雖然效能非常好,但是如果在舊瀏覽器上使用則需要 polyfill
- blazy 也是一個輕量級的懶載入庫(僅1.4kb)。和 lazysizes 一樣,載入它不需要任何的第三方工具,而且相容 IE7+。但缺點是其實現不基於 intersection observer
- yall.js 是我(原文作者)基於 IntersectionObserver 和 event handlers 寫的一個庫,相容 IE11 和主流瀏覽器
- 如果你在尋找一個基於 React 的懶載入庫,那可以考慮一下 react-lazyload 。儘管其沒有使用 intersection observer,但提供了一個類似的方法實現圖片的懶載入
上述的每一個懶載入庫都有非常棒的文件以及滿足你各種懶載入行為的標記模式(markup patterns)。
可能出錯的地方
儘管圖片和視訊的懶載入具有積極的、可衡量的效能優勢,但這並不是一項可以掉以輕心的任務。如果你做錯了,可能會有意想不到的後果。因此,記住以下幾點很重要:
Mind the fold
用 JavaScript 對頁面上的每一個媒體資源進行懶載入是很誘人的,但你要抵制這種誘惑。任何能在一屏內顯示的資源都不應該被懶載入,這樣的資源應該被視為關鍵資源,正常載入即可。
用正常方式而非懶載入的方式去載入關鍵資源的主要理由是因為懶載入是直到指令碼完成載入並開始執行時,才會延遲載入這些資源。對於視口之外(below the fold)的圖片,採用懶載入時可以的,但是對於視口之內(above the fold)的圖片使用標準的 <img>
元素能更快的載入關鍵資源。
此外,如果對觸發資源懶載入的條件沒有嚴格要求,則更理想的做法是在視口之外建立緩衝區,以便在使用者將它們滾動到視口之前開始載入圖片。例如,在建立 IntersectionObserver
例項時,intersection observer API 允許在可選物件中指定一個 rootMargin
屬性,這會給目標元素建立一個緩衝區,在元素進入視口之前觸發懶載入:
let lazyImageObserver = new IntersectionObserver(function(entries, observer) { // Lazy loading image code goes here }, { rootMargin: "0px 0px 256px 0px" });
rootMargin
屬性的定義方式和 CSS 的 margin
屬性型別,用於定義根元素(預設是瀏覽器視口,可以通過 root
屬性指定一個具體元素)的 margin
。上述程式碼中 rootMargin
的定義表示當圖片與視口元素底部的距離小於 256px 時,圖片便會開始載入。
佈局的改變和佔位
如果沒有使用佔位元素,那麼當圖片或視訊正在載入時會改變原有的頁面佈局,這不僅會犧牲使用者體驗,也會帶來昂貴的 DOM 佈局操作。因而最基本的方式是使用一個單色的佔位元素佔據和目標圖片元素一樣尺寸的大小,或者使用類似 LQIP 或 SQIP 的技術來暗示使用者這是一個媒體資源類的元素。
對於 <img>
元素,其 src
屬性應該初始化為一個佔位圖片,直到該屬性值更新為最終需要顯示圖片的 URL;對於 <video>
元素,可以利用 poster
屬性指向一個佔位圖片。此外,為 <img>
和 <video>
元素新增 width
和 height
屬性,確保在資源載入時,從佔位圖片向最終圖片過渡時不會改變已渲染的大小。
影象解碼延遲
通過 JavaScript 載入大圖並將其放入到 DOM 中時會佔用主執行緒,而大圖解碼時可能會造成短時間內的使用者互動無響應現象, 通過 decode
方法非同步進行圖片解碼 之後再將圖片元素插入到 DOM 中能有效避免這種現象。但並不是所有瀏覽器均支援 decode
方法,所以在使用前要先進行功能校驗:
var newImage = new Image(); newImage.src = "my-awesome-image.jpg"; if ("decode" in newImage) { // Fancy decoding logic newImage.decode().then(function() { imageContainer.appendChild(newImage); }); } else { // Regular image load imageContainer.appendChild(newImage); }
當資源沒有載入
媒體資源並不總能載入成功,當資源載入失敗時該怎麼處理呢?這完全取決於你,但你應該對這種情況有一個備份計劃。對於圖片,可能的處理方式如下:
var newImage = new Image(); newImage.src = "my-awesome-image.jpg"; newImage.onerror = function(){ // Decide what to do on error }; newImage.onload = function(){ // Load the image };
JavaScript 的可用性
我們不應該總假設 JavaScript 是可用的。如果你打算對圖片進行懶載入,可以考慮提供 <noscript>
標記在 JavaScript 不可用的情況下顯示圖片:
<!-- An image that eventually gets lazy loaded by JavaScript --> <img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load.jpg" alt="I'm an image!"> <!-- An image that is shown if JavaScript is turned off --> <noscript> <img src="image-to-lazy-load.jpg" alt="I'm an image!"> </noscript>
如果 JavaScript 不可用,使用者會同時看到佔位圖片和 <noscript>
標記內的圖片,因而,我們需要在 <html>
元素上新增一個 no-js
的 CSS 類:
<html class="no-js">
然後,我們在 <head>
元素內新增一行內聯指令碼用於移除 <html>
元素的 no-js
類,並且指令碼要置於 <link>
標籤之前:
<script>document.documentElement.classList.remove("no-js");</script>
最後,當 JavaScript 不可用時,我們可以使用一些簡單的 CSS 規則來隱藏需要懶載入的元素:
.no-js .lazy { display: none; }
總結
對圖片和視訊進行懶載入能有效降低應用的初始載入時間和負荷,使用者也不需要擔心非必要的網路行為和處理他們永遠也不會看到的媒體資源的消耗,但仍可以看到他們想看到的資源。
就效能提升的技術而言,懶載入是一種沒有任何爭議的方式。如果你的網站有大量的內聯圖片,懶載入是一種有效降低非必要下載消耗的技術,你的網站使用者和專案利益相關者也都會因此而感激你為提升效能而做出的努力。