為服務工作人員提供 Google 搜尋

講述各項原告、如何衡量影響,以及做出的權衡與利弊。

背景

在 Google 中搜尋任何主題,系統就會顯示值得識別的即時頁面,列出具有參考價值的相關搜尋結果。您或許「不太」會發現這個搜尋結果網頁是在特定情況下,由名為「服務工作人員」強大的網路技術提供。

在不對 Google 搜尋推出 Service Worker 支援的同時,數十位工程師在多個團隊間工作也能兼顧效能。這就是已出貨的項目、效能的測量方式,以及各項優缺點的故事。

探索 Service Worker 的主要原因

就像對網站進行任何架構變更一樣,在網頁應用程式中新增 Service Worker 時,應建立一組明確的目標。對於 Google 搜尋團隊而言,新增 Service Worker 有幾項重要原因值得探索。

有限的搜尋結果快取

Google 搜尋團隊發現,使用者通常會在短時間內多次搜尋同一個字詞。搜尋團隊不想只為了取得可能相同的結果而觸發新的後端要求,而是想要利用快取,並在本機執行這些重複要求。

即時性的重要性無法折扣。有時因為主題不斷演進,使用者期待看到最新結果,因而重複搜尋相同字詞。使用 Service Worker 可讓搜尋團隊實作精細的邏輯來控管本機快取搜尋結果的生命週期,進而達到他們認為最好提供服務的速度與更新頻率的平衡。

有意義的離線體驗

此外,Google 搜尋團隊也想提供有意義的離線體驗。當使用者想瞭解某個主題時,他們會希望直接前往 Google 搜尋頁面開始搜尋,不必擔心網際網路連線的問題。

如果沒有 Service Worker,在離線時造訪 Google 搜尋頁面只會導向瀏覽器的標準網路錯誤頁面,而使用者必須記得在連線恢復後再試一次。透過 Service Worker,您可以提供自訂的離線 HTML 回應,並讓使用者立即輸入搜尋查詢。

背景重試介面的螢幕截圖。

只有在連上網際網路後,系統才會顯示結果,但服務工作站會延遲搜尋作業,並在裝置使用背景同步 API 連上網路時,將搜尋延後並傳送至 Google 伺服器。

更聰明的 JavaScript 快取及服務

另一個動機是,將模組化 JavaScript 程式碼的快取和載入作業最佳化,讓搜尋結果網頁支援各種類型的功能。在沒有服務工作人員參與的情況下,JavaScript 套裝組合有很多好處,因此搜尋團隊不會只想完全停止套裝組合。

搜尋團隊藉由在執行階段建立 JavaScript 片段版本及快取精細區塊的能力,這或許能減少快取流失次數,並確保日後重複使用的 JavaScript 都能有效率地快取。服務工作站內的邏輯可針對包含多個 JavaScript 模組的組合分析傳出的 HTTP 要求,然後結合多個本機快取的模組以執行該要求,並盡可能有效「拆分」。這樣不但能節省使用者頻寬,還可改善整體回應速度。

使用服務工作站提供的快取 JavaScript 也會帶來效能優勢。在 Chrome 中,系統會儲存及重複使用該 JavaScript 剖析的位元組程式碼表示法,進而減少需要在執行階段執行才能在網頁上執行 JavaScript 的工作。

挑戰與解決方案

為達成團隊指定的目標,必須克服一些挑戰。儘管其中一些挑戰與 Google 搜尋有關,但其中有些挑戰適用於各種可能考慮服務工作人員部署的網站。

問題:服務工作站負擔

最大的挑戰是,在 Google 搜尋中啟用 Service Worker,其中一項真正阻礙,是確保不採取任何行動會增加使用者感知延遲時間。Google 搜尋將認真看待效能,以往若新功能對特定使用者族群造成了數十毫秒的額外延遲時間,Google 搜尋先前就封鎖新功能的推出作業。

當團隊在早期實驗期間開始收集成效資料時,就會發現有問題。為回應搜尋結果網頁的導覽要求而傳回的 HTML 是動態性質,會因需要在搜尋網路伺服器上執行的邏輯而大幅改變。目前無法讓服務工作站複製這個邏輯並立即傳回快取 HTML;最佳做法是將導覽要求傳送至後端網路伺服器,而這需要網路要求。

如未設定 Service Worker,這個網路要求會在使用者瀏覽時立即發生。註冊 Service Worker 後,系統一律會啟動它,並有機會執行其 fetch 事件處理常式,即使這些擷取處理常式不太可能會前往網路也一樣。啟動及執行服務工作站程式碼所需的時間,都會增加每個導覽頂端的額外負荷:

插圖:SW 啟動阻斷導覽要求。

這會造成 Service Worker 實作的延遲時間過度短,因此不得以合理方式證明其他優勢。此外,該團隊也發現,根據在實際裝置上的 Service Worker 啟動時間測量結果,啟動時間會廣泛,其中某些低階行動裝置在為結果頁面的 HTML 發出網路要求時,花了最多的時間來啟動 Service Worker。

解決方案:使用導覽預先載入

要讓 Google 搜尋團隊先行移動服務工作工作站,其中一項最重要的功能就是「導覽預先載入」。對任何需要使用網路回應來滿足導覽要求的 Service Worker 來說,使用導覽預先載入是一大優點。這會提示瀏覽器在服務工作站啟動時,立即開始發出導航要求:

插圖:SW 啟動程序與導航要求同時完成。

只要服務工作站啟動的時間比取得網路傳回回應所需的時間少,應該就不應是服務工作站造成的任何延遲負擔。

此外,搜尋團隊也必須避免在低階行動裝置上使用 Service Worker,以免服務工作站啟動時間超過導航要求。由於「低階」裝置沒有任何硬式規則,因此能夠為裝置檢查已安裝的 RAM 總量。記憶體容量小於 2 GB 的項目都會歸類為低階裝置類別,在這種情況下,服務工作站啟動時間將無法接受。

由於要快取的所有資源組合供日後使用,因此可用儲存空間是另一項考量因素,因此可快取的資源量可達數 MB。navigator.storage 介面可讓 Google 搜尋頁面事先得知,使用者嘗試快取資料時,是否會因儲存空間配額失敗而失敗的風險。

搜尋團隊留下了多項條件,這些條件可用來判斷是否要使用 Service Worker:如果使用者是透過支援導覽預先載入的瀏覽器前往 Google 搜尋頁面,且擁有至少 2 GB 的 RAM,且擁有足夠的儲存空間,就表示已註冊 Service Worker。不符合這些條件的瀏覽器或裝置不會視為服務工作站,但使用者仍可照常看到的 Google 搜尋體驗。

這種選擇性註冊的優點之一,就是能夠提供規模較小、效率更高的服務工作站。以相當新式的瀏覽器執行 Service Worker 程式碼,可避免舊版瀏覽器的轉譯和 polyfill 造成的負擔。從服務工作站的實作總大小中,最終減少了約 8 KB 的未壓縮 JavaScript 程式碼。

問題:Service Worker 範圍

當搜尋團隊執行足夠的延遲時間實驗,並確信使用導覽預先載入能為使用 Service Worker 提供可行且延遲中立的路徑後,便開始遇到一些實際問題。其中一個問題與 Service Worker 的範圍範圍規則有關。Service Worker 的範圍會決定它可以控制哪些頁面。

範圍是根據網址路徑前置字串運作。對於代管單一網頁應用程式的網域而言,並不會造成任何問題,因為您通常會使用最大範圍為 / 的 Service Worker (可控制網域下的任何網頁)。 但 Google 搜尋的網址結構有點複雜。

如果 Service Worker 達到了 / 的最大範圍,最終將能控制 www.google.com 之下代管的任何網頁 (或地區對等),而且該網域下有的網址都與 Google 搜尋無關。更合理的範圍是 /search,這樣至少會移除與搜尋結果完全無關的網址。

遺憾的是,即使不同變種的 Google 搜尋結果版本之間還是共用 /search 網址路徑,網址查詢參數則會決定要顯示哪類搜尋結果。有些變種版本使用的程式碼集與傳統網頁搜尋結果網頁完全不同。舉例來說,「圖片搜尋」和「購物搜尋」都會在 /search 網址路徑底下提供,但查詢參數不同,但這兩個介面都尚未準備好提供專屬的 Service Worker 體驗。

解決方案:建立分派和轉送架構

雖然有一些提案,允許比網址路徑前置字串更強大的功能來確定服務工作站範圍,但 Google 搜尋團隊在部署的服務工作站時,無法順利於其控制的某些頁面中執行操作。

為瞭解決這個問題,Google 搜尋團隊打造了一個自訂的調度和轉送架構,可用於檢查用戶端頁面的查詢參數等條件,並藉此決定要執行哪個特定程式碼路徑。系統設計並非透過硬式編碼規則,而是設計出靈活的系統,讓共用網址空間 (例如「圖片搜尋」和「購物搜尋」) 的團隊能分行執行,並決定實作自己的 Service Worker 邏輯。

問題:個人化搜尋結果和指標

使用者可使用 Google 帳戶登入 Google 搜尋,系統可根據使用者的特定帳戶資料,提供自訂搜尋結果體驗。 系統會根據特定的瀏覽器 Cookie 識別已登入的使用者。瀏覽器 Cookie 是備受廣泛支援且廣受支援的標準。

但使用瀏覽器 Cookie 的缺點之一,就是這類 Cookie 不會在 Service Worker 內部公開,而且無法自動檢查其值,並確保使用者未因使用者登出或切換帳戶而變更。(目前仍在努力將 Cookie 存取權授予服務 Worker,但截至本文撰寫期間,這個做法仍處於實驗階段,且未受到廣泛支援)。

如果 Service Worker 顯示的目前登入使用者和實際登入 Google 搜尋網頁介面的使用者檢視畫面不符,可能會導致不正確的個人化搜尋結果,或是來源有誤的指標和記錄。這些失敗情況都會對 Google 搜尋團隊造成嚴重問題。

解決方案:使用 postMessage 傳送 Cookie

Google 搜尋團隊不必等待實驗性 API 啟動,並直接在 Service Worker 中存取瀏覽器的 Cookie,而是採取了停止差距解決方案:每當載入服務工作處理程序所控管的網頁時,頁面會讀取相關的 Cookie,並使用 postMessage() 將 Cookie 傳送至 Service Worker。

然後,Service Worker 會根據預期的值檢查目前的 Cookie 值,如果不相符,就會採取步驟從儲存空間中清除所有使用者專屬資料,並在沒有錯誤個人化的情況下重新載入搜尋結果網頁。

服務工作處理程序將內容重設為基準時,採取的具體步驟與 Google 搜尋的需求有關,但對於其他處理從瀏覽器 Cookie 建立資料集的個人化資料的開發人員來說,您也可以採取相同的一般做法。

問題:實驗與流派

如前所述,Google 搜尋團隊十分仰賴在實際工作環境中執行實驗、在實際環境中測試新程式碼和功能的影響,然後再預設啟用這些功能。對大量使用快取資料的靜態服務工作站來說,這可能是一項挑戰,因為使用者選擇加入或退出實驗通常需要與後端伺服器通訊。

解決方案:動態產生的 Service Worker 指令碼

團隊嘗試的解決方案是使用由網路伺服器自訂的動態產生的服務工作站指令碼,而非預先產生的單一靜態 Service Worker 指令碼。至於可能影響服務工作站行為或一般網路要求的實驗,會直接包含在這個自訂服務工作站指令碼中。您可以透過結合傳統技術 (例如瀏覽器 Cookie) 來變更使用者的有效體驗組合,並在已註冊的 Service Worker 網址中提供更新後的程式碼。

使用動態產生的 Service Worker 指令碼,也可讓您輕鬆提供應避免的嚴重錯誤,以免在極少數情況下,服務工作站實作造成的嚴重錯誤。動態伺服器工作站回應可能是免人工管理實作,可以有效停用部分或所有目前使用者的 Service Worker。

問題:協調更新作業

在實際服務工作站部署方面,所面臨的最艱鉅挑戰之一,是在避免使用網路而偏好快取的情況下,同時確保現有使用者可在部署至實際工作環境後立即取得重大更新和變更。最佳平衡取決於許多因素:

  • 網頁應用程式是否為長期運作的單一頁面應用程式,使用者可無限期地保持開啟狀態,而不會前往新頁面。
  • 後端網路伺服器更新的部署頻率。
  • 一般使用者願意接受稍微過時的網頁應用程式,還是一律優先使用更新版。

對服務工作人員進行實驗時,Google 搜尋團隊會確保實驗在許多排定的後端更新作業中持續進行,確保指標和使用者體驗更能符合使用者實際看到的結果。

解決方案:在更新間隔和快取使用率之間取得平衡

測試多種不同的設定選項後,Google 搜尋團隊發現以下設定可在更新頻率和快取使用率之間取得適當平衡。

Service Worker 指令碼網址會透過 Cache-Control: private, max-age=1500 (1500 秒或 25 分鐘) 回應標頭提供,並在 updateViaCache 設為 'all' 進行註冊,確保遵循標頭。Google 搜尋網路後端,可能就是您想像的,一組遍布全球各地的大型伺服器,需要盡可能接近 100% 的運作時間。部署會影響 Service Worker 指令碼內容的變更會以滾動方式完成。

如果使用者開啟了已更新的後端,接著快速瀏覽至另一個網頁,而該頁面指向尚未收到更新服務工作站的後端,最後就可能多次切換不同版本。因此,如果自上次檢查以來沒有顯著缺點,則建議瀏覽器只在已經過 25 分鐘時檢查更新的指令碼。選擇加入這項行為的缺點,在動態產生 Service Worker 指令碼的端點接收到的流量會大幅減少。

此外,ETag 標頭是在 Service Worker 指令碼的 HTTP 回應上設定,可確保在通過 25 分鐘的更新檢查後,如果服務工作站在這段時間內未部署任何更新,伺服器就能有效率地回應 HTTP 304 回應。

雖然 Google 搜尋網頁應用程式中的部分互動只使用單一頁面應用程式樣式導覽 (透過 History API),但大致上來說,Google 搜尋是採用「真實」導覽功能的傳統網頁應用程式。當團隊決定使用兩個可加速服務工作站更新生命週期的選項時,就會發揮效用:clients.claim()skipWaiting()。點擊 Google 搜尋的介面通常最後會找到新的 HTML 文件。呼叫 skipWaiting 可確保更新後的 Service Worker 可在安裝後立即處理這些新的導覽要求。同樣地,呼叫 clients.claim() 表示更新後的 Service Worker 有機會在啟用 Service Worker 後,開始控制任何未控制的已開啟 Google 搜尋頁面。

Google 搜尋採用的方法不一定適合所有人,而是謹慎地對各種服務選項組合進行 A/B 測試的結果,直到找出最適合自己的方法為止。如果開發人員的後端基礎架構允許開發人員更快部署更新,可能會希望瀏覽器一律略過 HTTP 快取,提高瀏覽器檢查更新的 Service Worker 指令碼的頻率。如果您要建構單一頁面應用程式,使用者可能會長時間保持開啟狀態,因此使用 skipWaiting() 可能不是理想選擇。如果您允許在存在長期的用戶端時啟用新的 Service Worker,則可能會在快取出現不一致的問題中造成風險。

重點回顧

根據預設,Service Worker 的效能並不會偏差

在網頁應用程式中新增 Service Worker,代表插入額外的 JavaScript,也就是在網頁應用程式收到要求回應之前,需要載入並執行的 JavaScript。如果這些回應最後是來自本機快取,而不是來自網路,那麼與優先進行快取的效能相比,執行 Service Worker 的負擔通常微乎其微。但是,如果您知道服務工作站在處理導覽要求時都必須查詢網路,那麼使用導覽預先載入,對效能至關重要。

服務工作人員 (尚未!) 是漸進式的改進

如今 Service Worker 支援流程,比過去一年更明亮。所有新式瀏覽器現在至少都會具備部分服務工作站支援,但幸好,部分進階 Service Worker 功能 (例如背景同步處理和導覽預先載入) 尚未全面推出。對您所知道的特定功能子集進行功能檢查,且只在有必要時註冊 Service Worker,仍是合理的做法。

同樣地,如果您在外面進行實驗,也知道低階裝置最後會因為服務 Worker 的額外負擔,執行效能不佳的情況,那麼您也不必在這類情況下註冊 Service Worker。

在符合所有必要條件時,服務 worker 能為使用者體驗和整體載入效能帶來正面影響,您應該繼續將服務工作站視為「漸進式增強」,應用程式即會新增該功能。

評估一切

如要判斷送貨服務工作人員是否會對使用者體驗產生正面或負面影響,唯一的方法就是進行實驗,並評估結果。

設定有意義的評估方式,取決於您使用的分析供應商,以及您平常在部署設定中執行實驗的方式。請參閱這份個案研究,根據在 Google I/O 網頁應用程式中使用服務工作站的經驗,詳細說明使用 Google Analytics (分析) 收集指標的方法。

非目標

雖然許多網站開發社群中,許多服務人員會與漸進式網頁應用程式建立關聯,但建構「Google 搜尋 PWA」並不是我們的初始目標。Google 搜尋網頁應用程式目前不會透過網頁應用程式資訊清單提供中繼資料,也不會鼓勵使用者完成新增至主畫面流程。搜尋團隊目前對透過 Google 搜尋的傳統進入點造訪網頁應用程式的使用者感到滿意。

因此,與其嘗試將 Google 搜尋的網頁體驗,變成期望的已安裝應用程式所能滿足的期望,而是逐步展開初始網站,以逐步改善現有網站。

特別銘謝

感謝整個 Google 搜尋網頁開發團隊協助員工實作服務工作人員,以及分享本文撰寫的背景資料。特別感謝 Philippe Golle、Rajesh Jagannathan、R. Samuel Klatchko、Andy Martone、Leonardo Peña、Rachel Shearer、Greg Terrono 和 Clay Woolam。

更新 (2021 年 10 月):自本文發布以來,Google 搜尋團隊重新評估了現有 Service Worker 架構的優點和優缺點。上述 Service Worker 即將淘汰。隨著 Google 搜尋的網頁基礎架構不斷演進,該團隊可能會重新審視服務工作人員的設計。