推測性模組抓取
Qwik 能夠極速載入頁面並變得具有互動性,這歸功於它能夠在「沒有」JavaScript 的情況下啟動。除此之外,推測性模組抓取是一項強大的功能,允許 Qwik 在背景執行緒中預先填入瀏覽器的快取。
Qwik 的目標是通過僅根據潛在的用戶交互緩存應用程序的必要部分來優化加載。 它通過了解哪些交互是不可能的來避免加載不必要的捆綁包。
預填充緩存
每次頁面加載都會使用用戶當前時刻在頁面上*可能*執行的捆綁包預填充緩存。 例如,假設頁面上有一個按鈕的點擊監聽器。 當頁面加載時,Service Worker 的首要任務是確保該點擊監聽器的代碼已經在 緩存 中。 當用戶點擊按鈕時,Qwik 會向事件監聽器的函數和執行該函數所需的任何代碼依賴項發出請求。 目標是讓代碼已經在 瀏覽器緩存 中準備好執行。
初始頁面加載會為下一次可能的交互準備緩存,並在單獨的線程中逐步下載其他必要的代碼。 當發生後續交互(例如打開模態框或菜單)時,Qwik 將發出另一個事件,其中包含自上次交互以來*可能*使用的其他代碼。 隨著用戶與應用程序的交互,預填充緩存會持續進行。
預填充緩存事件
推薦的策略是使用 Service Worker 來填充 瀏覽器緩存。 Qwik 框架本身應該使用 prefetchEvent 實現,這已經是默認設置。
使用 Service Worker 預填充緩存
傳統上,Service Worker 用於緩存應用程序使用的大部分或全部捆綁包。 Service Worker 通常僅被視為使應用程序脫機工作的一種方式。
Qwik City 以完全不同的方式使用 Service Worker 來提供強大的緩存策略。 目標不是下載整個應用程序,而是使用 Service Worker 動態地預填充緩存,其中包含*可能*執行的內容。 通過*不*下載整個應用程序,釋放了資源,使用戶能夠僅請求他們完成屏幕上的當前任務*可能*需要的必要部分。
此外,Service Worker 將自動為從 Qwik 發出的這些事件添加監聽器。
後台任務
使用 Service Worker 的一個優點是它也是 Worker 的擴展,它在後台線程中運行。
Web Worker 使得在與 Web 應用程序的主執行線程分開的後台線程中運行腳本操作成為可能。 這樣做的優點是可以讓繁重的處理在單獨的線程中執行,從而允許主線程(通常是 UI 線程)在不被阻塞/減慢的情況下運行。
通過從 Service Worker(它是一個 Worker)內部預填充緩存,我們能夠在後台任務中執行代碼,從而不會干擾主 UI 線程。 通過不干擾主線程,我們可以提高 Qwik 應用程序對用戶的性能。
交互式預填充緩存
Qwik 本身應配置為使用 prefetchEvent 實作。這是預設值。當 Qwik 發出事件時,服務工作線程註冊會將事件資料主動轉發到已安裝且作用中的服務工作線程。
服務工作線程在背景執行緒中執行,會提取模組並將其新增至瀏覽器的 快取。主執行緒只需要發出有關所需套件組合的資料,而服務工作線程的唯一重點是快取這些套件組合。
- 如果瀏覽器已經快取了它?太好了,什麼都不用做!
- 如果瀏覽器尚未快取此套件組合,則讓我們開始提取請求。
服務工作線程確保對同一個套件組合的多個請求 不會同時發生。
快取請求和回應對
在許多傳統框架中,首選策略是使用 html <link>
標籤,並將 rel
屬性設定為 prefetch
、preload
或 modulepreload
。然而,由於 已知問題,Qwik 避免使用這種方法作為預設的預提取策略,但如果需要,它仍然可以 配置。
相反,Qwik 更傾向於使用一種更新的方法,該方法充分利用了瀏覽器的 快取 API,與 modulepreload 相比,它支援得更好。
快取 API
快取 API 通常與服務工作線程相關聯,是一種儲存請求和回應對的方法,以便應用程式能夠離線工作。除了使應用程式能夠在沒有網路連線的情況下工作之外,相同的快取 API 還為 Qwik 提供了一種非常強大的快取機制。
使用已安裝並啟用的 服務工作線程 來攔截請求,Qwik 能夠處理針對*已知*套件組合的特定請求。與服務工作線程的常見使用方法相比,預設情況下不會嘗試處理所有請求,只處理由 Qwik 生成的已知套件組合。網站安裝的服務工作線程仍然可以 由每個網站自訂。
Qwik 優化器的一個優點是它會生成一個 q-manifest.json
檔案。q-manifest.json
包含一個詳細的模組圖,說明套件組合如何關聯以及每個套件組合中包含哪些符號。同樣的模組圖資料會提供給服務工作線程,允許快取處理針對已知套件組合的每個網路請求。
動態匯入和快取
當 Qwik 請求模組時,它會使用動態 import()
。例如,假設發生了使用者互動,需要 Qwik 對 /build/q-abc.js
執行動態匯入。執行此操作的程式碼如下所示
const module = await import('/build/q-abc.js');
這裡重要的是,Qwik 本身並不知道預提取或快取策略。它只是在對 URL 發出請求。然而,因為我們安裝了一個服務工作線程,並且服務工作線程正在攔截請求,所以它能夠檢查 URL 並說:「看,這是對 /build/q-abc.js
的請求!這是我們的套件組合之一!在我們發出實際的網路請求之前,讓我們先檢查一下是否已經在快取中有了。」
這就是服務工作線程和快取 API 的強大之處!在另一個執行緒中,Qwik 會針對使用者可能很快就會請求的模組預先填入快取。如果這些模組已經被快取,那麼瀏覽器就什麼都不用做了。
網路請求平行化
在 快取請求和回應配對 文件中,我們解釋了 快取 和 服務工作執行緒 API 的強大組合。然而,在 Qwik 中,我們可以更進一步,確保不會為同一個套件建立重複的請求,並防止網路瀑布流,所有這些都在背景執行緒中完成。
避免重複請求
舉例來說,假設終端使用者目前的網路連線速度非常慢。當他們第一次請求載入頁面時,裝置會下載 HTML 並渲染內容(這是 Qwik 真正擅長的地方)。在這種網路連線速度慢的情況下,如果使用者必須再下載數百 KB 的資料才能 讓他們的應用程式運作並具有互動性,那就太可惜了。
然而,由於應用程式是使用 Qwik 建立的,因此終端使用者不需要下載整個應用程式就能使其具有互動性。相反地,終端使用者已經下載了 SSR 渲染的 HTML 應用程式,並且可以立即預先擷取任何互動部分,例如「加入購物車」按鈕。
請注意,我們只預先擷取實際的監聽器程式碼,而*沒有*預先擷取整個元件樹渲染函式的堆疊。
在這個非常常見的真實範例中,裝置的網路連線速度很慢,它會立即開始為終端使用者可見的可能互動預先填充快取。然而,由於網路連線速度慢,即使我們盡快在 背景執行緒 中開始快取模組,擷取請求本身可能仍在進行中。
為了示範,我們假設擷取這個套件需要兩秒鐘的時間。然而,在瀏覽頁面一秒鐘後,使用者點擊了按鈕。
在傳統的框架中,很有可能什麼事都不會發生!如果框架尚未完成下載、注水和重新渲染,則無法將事件監聽器新增到按鈕。這意味著使用者的互動將會遺失。
透過 Qwik 的快取策略,如果使用者點擊了一個按鈕,而我們在一秒鐘前已經發起了一個請求,並且距離完全接收只差一秒鐘,那麼終端使用者只需要等待那一秒鐘。請記住,在這個示範中,他們的網路連線速度很慢。幸運的是,使用者已經收到了完全渲染的載入頁面,並且已經在查看完整的頁面。接下來,他們只是用他們可以互動的應用程式部分預先填充快取,而他們緩慢的網路連線專用於這些套件。這與他們緩慢的網路連線下載整個應用程式,只是為了執行一個監聽器形成對比。
Qwik 可以攔截已知套件的請求。如果在背景執行緒中有一個擷取正在進行中,而使用者請求同一個套件,它將確保第二個請求能夠重複使用同一個套件,而該套件可能已經下載完成。嘗試使用 link 執行任何這些操作也說明了為什麼 Qwik 偏好使用快取 API 並使用服務工作執行緒作為預設值來攔截請求,而不是使用 link。
減少網路瀑布流
當多個請求依序發出時,就會發生網路瀑布流。這種循序漸進的過程會顯著降低效能,因為下載所有必要模組的時間會延長,與所有模組同時開始平行下載的情況相比。
以下是一個包含三個模組的範例:A、B 和 C。模組 A 匯入 B,而 B 匯入 C。HTML 文件首先請求模組 A,從而啟動瀑布流。
import './b.js';
console.log('Module A');
import './c.js';
console.log('Module B');
console.log('Module C');
<script type="module" src="./a.js"></script>
在此範例中,當第一次請求模組 A
時,瀏覽器並不知道它也應該開始請求模組 B
和 C
。它甚至不知道需要開始請求模組 B
,直到模組 A
下載完成後才知道。這是一個常見的問題,因為瀏覽器事先並不知道它應該開始請求什麼,直到每個模組都下載完成後才知道。
然而,因為我們的服務工作程序包含一個從清單生成的模組圖,我們確實知道接下來*將*會請求的所有模組。因此,當發生使用者互動或預取套件時,瀏覽器會啟動對*將*會請求的所有套件的請求。這使我們能夠大幅縮短請求所有套件所需的時間。
使用者服務工作程序代碼
由 Qwik City 安裝的預設服務工作程序仍然可以完全由應用程式控制。例如,原始程式碼檔案 src/routes/service-worker.ts
會變成 /service-worker.js
,這是瀏覽器請求的腳本。請注意它在 src/routes
中的位置如何仍然遵循相同的基於目錄的路由模式。
以下是一個預設 src/routes/service-worker.ts
原始程式碼檔案的範例
import { setupServiceWorker } from '@builder.io/qwik-city/service-worker';
setupServiceWorker();
addEventListener('install', () => self.skipWaiting());
addEventListener('activate', (ev) => ev.waitUntil(self.clients.claim()));
src/routes/service-worker.ts
的原始程式碼可以修改,包括選擇加入或選擇退出設定 Qwik City 的服務工作程序。
請注意,setupServiceWorker()
函數是從 @builder.io/qwik-city/service-worker
導入的,並在原始程式碼檔案的頂部執行。開發人員可以根據自己的需要靈活地修改此函數的調用時間和位置。例如,如果開發人員希望先處理提取請求,則可以在 setupServiceWorker()
上方添加他們的提取監聽器。或者,如果他們根本不想使用 Qwik City 的服務工作程序,則只需從檔案中移除 setupServiceWorker()
即可。
此外,預設的 src/routes/service-worker.ts
檔案帶有一個 安裝 和 啟動 事件監聽器,每個監聽器都添加到檔案的底部。提供的回調是建議的回調。但是,開發人員可以根據自己應用程式的需求修改這些回調。
另一個需要注意的重要事項是,Qwik City 的請求攔截*僅*適用於 Qwik 套件,它不會嘗試處理任何不屬於其構建過程的請求。
因此,雖然 Qwik City 確實提供了一種幫助預取和快取套件的方法,但它並未完全控制應用程式的服務工作程序。這仍然允許開發人員在不與 Qwik 衝突的情況下添加他們的服務工作程序邏輯。
開發期間停用
預測性模組提取僅在預覽或生產構建中才會生效。在開發過程中,服務工作程序會被停用,這也會停用預測性模組提取。這是因為在開發過程中,我們希望始終確保使用最新的開發代碼,而不是先前快取的代碼。
HTTP 快取與服務工作程序快取
預測性模組提取可能看起來沒有作用,部分原因是快取的級別不同。例如,瀏覽器本身可能會將請求快取在其 HTTP 快取 中,而服務工作程序可能會將請求快取在 快取 API 中。僅清空其中一個快取可能不足以看到預測性模組提取的效果。
誤導性的清空快取並強制重新載入
當開發人員執行 清空快取並強制重新載入 時,這有點誤導性,因為它實際上*僅*清空了瀏覽器的 HTTP 快取。但是,它並未清空服務工作程序的快取。即使瀏覽器的 HTTP 快取是空的,服務工作程序仍然擁有先前快取的請求。
此外,當使用「清空快取並強制重新載入」時,瀏覽器會在發送給伺服器的*請求*中發送一個 no-cache
快取控制標頭。由於請求包含 no-cache
快取控制標頭,服務工作線程會特意不使用自身的快取,而是讓瀏覽器再次執行通常的 HTTP 抓取。
清空服務工作線程快取
測試預測性模組抓取的建議方法是:
- 取消註冊服務工作線程:在 Chrome 開發者工具中,前往「應用程式」標籤,然後在「服務工作線程」下方,點擊網站服務工作線程的「取消註冊」連結。
- 刪除「QwikBuild」快取儲存空間:在 Chrome 開發者工具中,前往「應用程式」標籤,然後在左側的「快取儲存空間」下方,右鍵點擊「QwikBuild」快取儲存空間上的「刪除」。
- 不要強制重新載入:不要強制重新載入(這會向服務工作線程發送 no-cache 快取控制標頭),只需點擊網址列並按下 Enter 鍵。這將會發送一個如同您是首次訪客的正常請求。
請注意,此過程僅用於測試預測性模組抓取,新建置時並不需要執行此操作。每次建置都會建立新的服務工作線程,舊的服務工作線程將會自動取消註冊。
偵錯模式
Qwik 核心中使用 <PrefetchServiceWorker />
和 <PrefetchGraph />
元件(位於 root.tsx
中)的服務工作線程具有偵錯模式。
如要查看服務工作線程記錄,請在 JavaScript 主控台中新增 window.qwikPrefetchSW.push(['verbose', '', []])
並按下 Enter
鍵。