js13kGames:漸進式載入
在本教程的先前步驟中,我們介紹了有助於我們將 js13kPWA 示例應用製作為漸進式 Web 應用的 API,包括 Service Workers、Web Manifest 和 通知和推送。在本文中,我們將進一步透過漸進式載入其資源來提高應用的效能。
首次有意義的繪製
儘快向用戶呈現有意義的內容至關重要——他們等待頁面載入的時間越長,在全部載入完成前離開的可能性就越大。我們應該至少向他們展示頁面所需的基本檢視,並在稍後會載入更多內容的位置設定佔位符。
這可以透過漸進式載入實現——也稱為 延遲載入。其核心思想是儘可能推遲載入(HTML、CSS、JavaScript)資源,隻立即載入對首次體驗至關重要的部分。
打包與拆分
許多訪問者不會瀏覽網站上的每一個頁面,但通常的做法是將我們擁有的所有功能打包到一個大檔案中。一個 bundle.js 檔案可能包含數兆位元組,而一個單獨的 style.css 包可能包含從基本 CSS 結構定義到網站所有版本(移動、平板、桌面、僅列印等)的所有可能樣式。
將所有資訊作為單個檔案載入比載入許多小檔案更快,但如果使用者最初並不需要所有內容,我們可以只加載關鍵部分,然後在需要時管理其他資源。
渲染阻塞資源
打包是個問題,因為瀏覽器必須先載入 HTML、CSS 和 JavaScript,然後才能將渲染結果繪製到螢幕上。在初始訪問網站到載入完成的幾秒鐘內,使用者會看到一個空白頁面,這是一種糟糕的體驗。
為了解決這個問題,我們可以例如在 JavaScript 檔案中新增 defer 屬性
<script src="app.js" defer></script>
這些檔案將在文件本身解析完成 *之後* 下載和執行,因此不會阻塞 HTML 結構的渲染。
另一種技術是僅在需要時使用 動態 import 來載入 JavaScript 模組。
例如,如果網站有一個搜尋按鈕,我們可以在使用者單擊搜尋按鈕後加載搜尋功能的 JavaScript。
document.getElementById("open-search").addEventListener("click", async () => {
const searchModule = await import("/modules/search.js");
searchModule.loadAutoComplete();
});
一旦使用者單擊按鈕,就會呼叫非同步的點選處理程式。該函式會等待模組載入完成,然後呼叫該模組匯出的 loadAutoComplete() 函式。因此,search.js 模組僅在交互發生時才會被下載、解析和執行。
我們還可以拆分 CSS 檔案併為其新增媒體型別
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="print.css" media="print" />
這將指示瀏覽器僅在滿足條件時才載入它們。
在我們的 js13kPWA 演示應用中,CSS 非常簡單,可以直接放在一個檔案中,沒有特定的載入規則。我們可以做得更進一步,將 style.css 中的所有內容都移到 index.html 的 <head> 中的 <style> 標籤內——這將進一步提高效能,但為了示例的可讀性,我們將跳過這種方法。
影像
除了 JavaScript 和 CSS,網站通常還會包含許多影像。當你在 HTML 中包含 <img> 元素時,所有引用的影像都會在初始網站訪問期間被獲取和下載。在宣佈網站準備就緒之前下載數兆位元組的影像資料是很常見的,但這同樣會給人帶來糟糕的效能印象。我們在開始檢視網站時,並不需要所有影像都具備最佳質量。
這可以進行最佳化。首先,你應該使用類似於 TinyPNG 的工具或服務,它們可以在不顯著改變質量的情況下減小影像檔案大小。如果已經完成了這一步,那麼你可以考慮使用 JavaScript 最佳化影像載入。我們將在下面進行解釋。
佔位符影像
與其在 <img> 元素的 src 屬性中包含所有遊戲截圖(這將強制瀏覽器自動下載它們),不如透過 JavaScript 選擇性地載入。js13kPWA 應用使用佔位符影像,它小巧輕便,而最終影像的路徑則儲存在 data-src 屬性中。
<img src="data/img/placeholder.png" data-src="data/img/SLUG.jpg" alt="NAME" />
這些影像將在網站完成 HTML 結構構建 *之後* 透過 JavaScript 載入。佔位符影像的縮放方式與原始影像相同,因此它會佔據相同的空間,並且在影像載入時不會導致佈局重繪。
透過 JavaScript 載入
app.js 檔案處理 data-src 屬性,如下所示:
let imagesToLoad = document.querySelectorAll("img[data-src]");
const loadImages = (image) => {
image.setAttribute("src", image.getAttribute("data-src"));
image.onload = () => {
image.removeAttribute("data-src");
};
};
imagesToLoad 變數包含所有影像的引用,而 loadImages 函式將路徑從 data-src 移到 src。當每個影像實際載入完成後,我們刪除它的 data-src 屬性,因為它不再需要了。然後我們遍歷每個影像並載入它。
imagesToLoad.forEach((img) => {
loadImages(img);
});
CSS 中的模糊效果
為了使整個過程在視覺上更具吸引力,佔位符在 CSS 中被模糊處理。

我們最初渲染影像時帶有模糊效果,因此可以實現向清晰影像的過渡。
article img[data-src] {
filter: blur(0.2em);
}
article img {
filter: blur(0em);
transition: filter 0.5s;
}
這將在半秒鐘內消除模糊效果,對於“載入中”效果來說已經足夠好了。
按需載入
上面討論的影像載入機制工作得很好——它在渲染 HTML 結構後加載影像,並在此過程中應用了漂亮的過渡效果。問題在於它仍然一次性載入 *所有* 影像,儘管使用者在頁面載入時只會看到前兩三個。
這個問題可以透過僅在需要時載入影像來解決:這被稱為 *延遲載入*。 延遲載入 是一種僅當影像出現在視口中時才載入影像的技術。有幾種方法可以告訴瀏覽器延遲載入影像。
<img> 上的 loading 屬性
告訴瀏覽器延遲載入的最簡單方法不需要 JavaScript。只需在 <img> 元素中新增 loading 屬性,其值為 lazy,瀏覽器就會知道僅在需要時載入此影像。
<img
src="data/img/placeholder.png"
data-src="data/img/SLUG.jpg"
alt="NAME"
loading="lazy" />
交集觀察器
這是先前有效示例的漸進式增強—— Intersection Observer 將僅在使用者向下滾動導致目標影像顯示在視口中時才載入它們。
相關的程式碼看起來是這樣的:
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver((items, observer) => {
items.forEach((item) => {
if (item.isIntersecting) {
loadImages(item.target);
observer.unobserve(item.target);
}
});
});
imagesToLoad.forEach((img) => {
observer.observe(img);
});
} else {
imagesToLoad.forEach((img) => {
loadImages(img);
});
}
如果支援 IntersectionObserver 物件,則應用會建立一個新例項。作為引數傳遞的函式處理一個或多個項與觀察器相交(即出現在視口內)的情況。我們可以迭代每個情況並做出相應反應——當影像可見時,我們載入正確的影像並停止觀察它,因為我們不再需要觀察它。
讓我們重申一下我們之前提到的漸進式增強——程式碼的編寫方式使得應用無論是否支援 Intersection Observer 都能正常工作。如果不支援,我們只會使用前面介紹的更基本的方法來載入影像。
改進
請記住,有許多方法可以最佳化載入時間,而此示例僅探討了其中一種方法。你可以嘗試使你的應用更健壯,使其在沒有 JavaScript 的情況下也能工作——可以使用 <noscript> 來顯示已分配最終 src 的影像,或者將 <img> 標籤包裝在指向目標影像的 <a> 元素中,這樣使用者就可以在需要時單擊並訪問它們。
我們不會這樣做,因為應用本身依賴於 JavaScript——沒有它,遊戲列表甚至不會載入,Service Worker 程式碼也不會執行。
我們可以重寫載入過程,不僅載入影像,還載入完整的專案,包括完整的描述和連結。它將像無限滾動一樣工作——僅當用戶向下滾動頁面時才載入列表中的專案。這樣,初始 HTML 結構將非常小,載入時間會更短,我們將獲得更大的效能優勢。
總結
初始載入的檔案更少,檔案被拆分成模組,使用佔位符,並按需載入更多內容——這將有助於實現更快的初始載入時間,從而為應用建立者帶來好處,併為使用者提供更流暢的體驗。
請記住漸進式增強方法——無論裝置或平臺如何,都要提供一個可用的產品,但要確保為使用現代瀏覽器的使用者提供增強的體驗。
最後的思考
本教程系列到此結束——我們回顧了 js13kPWA 示例應用的原始碼,並學習了 PWA 結構、透過 Service Workers 實現離線可用性、可安裝的 PWA,以及最後 通知。
而在本文中,我們探討了漸進式載入的概念,包括一個利用 Intersection Observer API 的有趣示例。
歡迎隨意嘗試程式碼,用 PWA 功能增強你現有的應用,或自己構建全新的東西。PWA 相較於普通 Web 應用具有巨大的優勢。