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

說明已推出的內容、如何評估影響力,以及做出的取捨。

背景

只要在 Google 上搜尋任何主題,您就會看到一個立即可辨識的頁面,其中包含有意義且相關的結果。但您可能不曉得的是,在某些情況下,這個搜尋結果頁面是由稱為服務工作者的強大網路技術提供服務。

為了讓 Google 搜尋支援服務工作管理員,且不會對效能造成負面影響,我們需要數十位工程師跨團隊合作。這篇文章將說明我們推出了哪些功能、如何評估效能,以及做出了哪些取捨。

探索服務工作者的主要原因

就像對網站進行任何架構變更一樣,在網頁應用程式中新增服務工作者時,也應明確設定目標。對 Google 搜尋團隊而言,新增服務工作者值得探索的原因有幾個。

限制搜尋結果快取

Google 搜尋團隊發現,使用者經常在短時間內搜尋同一個字詞不只一次。搜尋團隊希望善用快取功能,在本機處理這些重複要求,而非只為了取得可能相同的結果而觸發新後端要求。

新鮮度的重要性不容小覷,有時使用者會重複搜尋相同字詞,因為這是一個不斷演變的話題,他們希望看到最新的結果。使用服務工作者,搜尋團隊就能實作精細的邏輯,控管本機快取搜尋結果的生命週期,並取得速度與新鮮度之間的最佳平衡,以便為使用者提供最佳服務。

有意義的離線體驗

此外,Google 搜尋團隊也希望提供實用的離線體驗。使用者想瞭解某個主題時,希望能直接前往 Google 搜尋頁面並開始搜尋,而不必擔心網路連線是否正常運作。

如果沒有服務工作者,離線時造訪 Google 搜尋頁面只會導向瀏覽器的標準網路錯誤頁面,使用者必須記得在連線恢復後再回來重試。使用服務工作者,您就能提供自訂離線 HTML 回應,並允許使用者立即輸入搜尋查詢。

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

只有在有網際網路連線時,才能取得結果,但服務工作者可讓搜尋作業延後執行,並在裝置使用背景同步處理 API 重新上線時,將結果傳送至 Google 伺服器。

更聰明的 JavaScript 快取和服務

另一個動機是,我們想針對搜尋結果頁面中各種不同類型的功能,最佳化模組化 JavaScript 程式碼的快取和載入作業。JavaScript 套件化可提供許多優點,在沒有服務工作者參與的情況下,這些優點就顯得合理,因此搜尋團隊不想完全停止套件化。

搜尋團隊利用服務工作者的功能,在執行階段為 JavaScript 版本和精細區塊建立快取,以減少快取轉換量,並確保日後重複使用的 JavaScript 能有效快取。服務工作者內部的邏輯可分析套件傳出 HTTP 要求,其中包含多個 JavaScript 模組,並透過拼湊多個在本機快取的模組來滿足要求,也就是盡可能「解組」。這麼做可節省使用者頻寬,並提高整體回應速度。

使用服務工作者提供的快取 JavaScript 也有效能優勢:在 Chrome 中,系統會儲存及重複使用該 JavaScript 的剖析過的位元組程式碼表示法,因此在執行階段執行網頁上的 JavaScript 時,需要完成的工作量會減少。

挑戰與解決方案

以下是團隊為了達成目標而必須克服的幾個難關。雖然其中有些挑戰是 Google 搜尋專屬的,但許多挑戰也適用於可能考慮部署服務工作者的各式各樣網站。

問題:Service worker 額外負擔

在 Google 搜尋中啟動服務工作者時,最大的挑戰 (也是唯一的阻礙) 就是確保服務工作者不會執行任何可能增加使用者感知延遲的操作。Google 搜尋對效能「非常」重視,過去曾經因為新功能會為特定使用者族群帶來數十毫秒的額外延遲,而禁止推出新功能。

當團隊開始在最早期的實驗中收集成效資料時,就發現可能會發生問題。系統會根據需要在 Google 搜尋網路伺服器上執行的邏輯,動態回應搜尋結果網頁的導覽要求,因此傳回的 HTML 會有所不同。目前服務工作站無法複製這項邏輯,也無法立即傳回快取的 HTML,因此只能將導覽要求傳遞給後端網頁伺服器,而這需要網路要求。

如果沒有服務工作者,這個網路要求會在使用者瀏覽時立即發生。註冊服務工作者時,您必須一律啟動服務工作者,並讓服務工作者有機會執行其 fetch 事件處理常式,即使這些擷取處理常式不會執行任何其他操作,也不例外。啟動及執行服務 worker 程式碼所需的時間,是每個導覽過程中純粹的額外負擔:

插圖:SW 啟動程序封鎖導覽要求。

這會導致服務工作站實作出現過多的延遲時間,無法提供任何其他好處。此外,團隊發現,根據在實際裝置上測量服務工作者啟動時間,啟動時間分布範圍很廣,有些低階行動裝置啟動服務工作者所需的時間,幾乎與網路要求結果頁面 HTML 所需的時間一樣長。

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

讓 Google 搜尋團隊能夠提前推出服務工作者功能的單一關鍵功能,就是導覽預先載入。對於需要使用網路回應來滿足導覽要求的任何服務工作者,使用導覽預先載入功能是提升效能的關鍵。它會向瀏覽器提供提示,讓瀏覽器在服務工作者啟動時立即開始提出導覽要求:

插圖:與導覽要求並行執行的 SW 啟動作業。

只要服務工作站啟動所需的時間少於從網路取得回應所需的時間,服務工作站就不會造成任何延遲開銷。

搜尋團隊也需要避免在低階行動裝置上使用服務工作站,因為在這些裝置上,服務工作站的啟動時間可能會超過導覽要求。由於「低階」裝置的定義並無明確規則,因此他們提出了「檢查裝置上安裝的總 RAM 容量」這個啟發式方法。記憶體容量低於 2 GB 的裝置都屬於低階裝置,因此服務工作站的啟動時間會過長。

可用的儲存空間也是另一個考量因素,因為要快取供日後使用的完整資源集合可能會達到數 MB。navigator.storage 介面可讓 Google 搜尋頁面提前判斷,在快取資料時是否會因儲存空間配額不足而失敗。

因此,搜尋團隊提供多項條件,可用於判斷是否要使用服務工作者:如果使用者使用支援導覽預先載入功能的瀏覽器前往 Google 搜尋頁面,且至少有 2 GB 的 RAM 和足夠的儲存空間,系統就會註冊服務工作者。不符合上述條件的瀏覽器或裝置不會產生服務工作者,但仍會看到與以往相同的 Google 搜尋體驗。

這種選擇性註冊的附帶好處,就是能夠提供更小、更有效率的服務工作程式。指定相當新式的瀏覽器來執行服務工作者程式碼,可消除舊版瀏覽器的轉譯和 polyfill 額外負擔。這項做法最終從服務工作者導入作業的總大小中,刪除約 8 千位元大小的未壓縮 JavaScript 程式碼。

問題:Service Worker 範圍

搜尋團隊進行足夠的延遲實驗,並確信使用導覽預先載入功能可提供可行且不受延遲影響的服務工作程使用途徑後,便開始著手解決一些實際問題。其中一個問題與服務工作者的範圍規則有關。Service Worker 的範圍會決定它可控管哪些網頁。

範圍設定會根據網址路徑前置字串運作。對於代管單一網頁應用程式的網域而言,這並不是問題,因為您通常只會使用 / 的最大範圍 Service Worker,而這可以控管網域下的任何網頁。不過,Google 搜尋的網址結構比較複雜。

如果 Service Worker 的範圍設為 /,Service Worker 就會能夠控制 www.google.com (或地區對應項目) 下代管的任何網頁,而該網域下有與 Google 搜尋無關的網址。/search 是較合理且限制較嚴格的範圍,至少可以排除與搜尋結果完全無關的網址。

很遺憾,即使 /search 網址路徑會在不同類型的 Google 搜尋結果中共用,但網址查詢參數會決定要顯示哪一種特定類型的搜尋結果。其中某些變種版本使用的程式碼集與傳統網頁搜尋結果頁面完全不同。舉例來說,圖片搜尋和購物搜尋都會透過 /search 網址路徑提供服務,且有不同的查詢參數,但這兩個介面都還未準備好提供各自的服務工作者體驗。

解決方案:建立調度和路由架構

雖然有一些提案允許使用比網址路徑前置字較強大的工具來判斷 Service Worker 範圍,但 Google 搜尋團隊在部署 Service Worker 時遇到問題,無法為其控制的部分網頁執行任何操作。

為解決這個問題,Google 搜尋團隊建立了專屬的調度和路由架構,可設定為檢查用戶端頁面的查詢參數等條件,並使用這些條件判斷要沿用哪個特定的程式碼路徑。系統並未使用硬式編碼規則,而是以彈性為設計目標,讓共用網址空間的團隊 (例如圖片搜尋和購物搜尋) 能夠在日後加入自己的服務工作者邏輯 (如果他們決定要實作)。

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

使用者可以使用 Google 帳戶登入 Google 搜尋,系統會根據他們的特定帳戶資料,提供個人化的搜尋結果體驗。系統會透過特定的瀏覽器 Cookie 來識別已登入的使用者,這是一種備受推崇且廣泛支援的標準。

不過,使用瀏覽器 Cookie 的缺點是,這些 Cookie 不會在 Service Worker 中公開,而且無法自動檢查其值,確保 Cookie 不會因使用者登出或切換帳戶而變更。(我們正在努力為服務工作者提供 Cookie 存取權,但截至本文撰寫時,這項做法仍處於實驗階段,且尚未廣泛支援)。

如果服務工作者對目前登入使用者的檢視畫面,與實際登入 Google 搜尋網頁介面的使用者不符,可能會導致不正確的個人化搜尋結果,或指派錯誤的指標和記錄。對 Google 搜尋團隊而言,任何失敗情況都會是嚴重的問題。

解決方法:使用 postMessage 傳送 Cookie

為了避免等待實驗性 API 推出,並提供服務工作者內瀏覽器 Cookie 的直接存取權,Google 搜尋團隊採用了暫時性解決方案:只要載入由服務工作者控制的網頁,該網頁就會讀取相關 Cookie,並使用 postMessage() 將其傳送至服務工作者。

接著,服務工作者會檢查目前的 Cookie 值是否與預期的值相符。如果不相符,服務工作者會採取步驟,從儲存空間中清除任何使用者專屬資料,並重新載入搜尋結果網頁,以免出現任何錯誤的個人化內容。

服務工作者要採取的具體步驟,才能將一切重設為基準,這項作業特別符合 Google 搜尋的規定,但其他開發人員處理以瀏覽器 Cookie 為依據的個人化資料時,也可能會用到相同的一般做法。

問題:實驗和動態

如前所述,Google 搜尋團隊在實際工作環境中執行實驗,並測試新程式碼和功能在實際情況下的效果,然後才將這些功能設為預設啟用。對於仰賴快取資料的靜態服務工作者而言,這可能會造成一些挑戰,因為選擇加入或退出實驗時,通常需要與後端伺服器進行通訊。

解決方案:動態產生的服務工作者指令碼

團隊採用的解決方案是使用動態產生的服務 worker 指令碼,由網路伺服器針對每位使用者進行自訂,而非預先產生的單一靜態服務 worker 指令碼。這類實驗資訊通常會影響服務工作者的行為或網路要求,並直接納入此自訂服務工作者指令碼。您可以透過結合傳統技術 (例如瀏覽器 Cookie),以及在已註冊的服務工作者網址中提供更新的程式碼,為使用者變更一系列的活躍體驗。

使用動態產生的服務工作者指令碼,也可以在服務工作者實作有需要避免的致命錯誤時,更輕鬆地提供逃逸方法。動態伺服器 worker 回應可以是無操作實作,有效地為部分或所有目前使用者停用服務 worker。

問題:協調更新

在實際的服務工作者部署作業中,最棘手的挑戰之一,就是在避免使用網路的情況下,為快取設計合理的權衡點,同時確保現有使用者在部署至正式環境後,能盡快取得重要更新和變更。適當的平衡取決於許多因素:

  • 您的網頁應用程式是否為長效型單頁應用程式,使用者可無限期保持開啟狀態,而無須前往新頁面。
  • 後端網頁伺服器更新的部署週期。
  • 一般使用者是否會容許使用稍舊的網頁應用程式版本,或是新版本是否是首要考量。

在測試服務工作站時,Google 搜尋團隊會確保在多個預定的後端更新中持續執行實驗,以確保指標和使用者體驗更貼近實際使用者在實際情況中看到的內容。

解決方案:平衡新鮮度和快取利用率

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

服務工作者指令碼網址會搭配 Cache-Control: private, max-age=1500 (1500 秒,或 25 分鐘) 回應標頭提供,並將 updateViaCache 設為「all」註冊,以確保系統會遵循標頭。如您所知,Google 搜尋網頁後端是遍布全球的大型分散式伺服器集合,需要盡可能達到 100% 的正常運作時間。部署會影響服務工作者指令碼內容的變更時,請採取逐步方式進行。

如果使用者觸及已更新的後端,然後快速前往另一個觸及尚未收到更新服務 worker 的後端的網頁,最終會在多個版本之間來回切換。因此,如果上次檢查後 25 分鐘內,瀏覽器只需檢查是否有更新的腳本,就不會造成太大的負面影響。採用這項行為的好處,就是可大幅減少動態產生服務工作者指令碼的端點所收到的流量。

此外,服務工作者指令碼的 HTTP 回應會設定 ETag 標頭,確保在 25 分鐘過後進行更新檢查時,如果在此期間內未有任何服務工作者部署更新,伺服器就能以 HTTP 304 回應進行有效回應。

雖然 Google 搜尋網頁應用程式中的部分互動會使用單頁應用程式樣式的導覽 (也就是透過 History API),但 Google 搜尋大多是使用「實際」導覽的傳統網頁應用程式。當團隊決定使用兩種加速服務工作者更新生命週期的選項時,就會啟用此功能:clients.claim()skipWaiting()。在 Google 搜尋介面中點選各處,通常會導向新的 HTML 文件。呼叫 skipWaiting 可確保更新的服務工作者在安裝後立即處理這些新的導覽要求。同樣地,呼叫 clients.claim() 表示在服務工作者啟用後,更新後的服務工作者有機會開始控制任何未受控的 Google 搜尋網頁。

Google 搜尋採用的做法不一定適合所有人,而是經過仔細的 A/B 測試,針對各種服務選項組合進行測試,直到找到最適合的組合。如果後端基礎架構可讓開發人員更快部署更新,他們可能會希望瀏覽器一律忽略 HTTP 快取,盡可能頻繁地檢查更新的服務工作者指令碼。如果您要建構單頁應用程式,且使用者可能會長時間保持開啟狀態,那麼使用 skipWaiting() 可能不是適合您的選擇。如果您允許新的服務工作者在有長效型用戶端的情況下啟用,就可能發生快取不一致的情形

注意事項

根據預設,服務工作者不會影響效能

在網頁應用程式中加入 Service Worker,代表插入額外的 JavaScript,需要在網頁應用程式收到要求回應前載入及執行。如果這些回應最終來自本機快取,而非網路,那麼相較於快取優先的效能優勢,執行服務工作者的額外負擔通常可以忽略不計。不過,如果您知道服務工作者在處理導覽要求時,必須一律諮詢網路,那麼使用導覽預先載入功能,就是一項重要的效能勝利方程式。

服務工作程式 (仍是!) 漸進式增強功能

服務工作者支援功能的發展比起一年前,如今更為亮眼。目前所有新型瀏覽器都至少提供部分服務工作程支援,但不幸的是,部分進階服務工作程功能 (例如背景同步處理和導覽預先載入) 並未全面推出。針對您知道需要的特定子集功能進行功能檢查,並只在這些功能存在時註冊服務工作者,仍是合理的做法。

同樣地,如果您在實際環境中進行實驗,並知道低階裝置在服務工作站的額外額外負擔下,最終會導致效能不佳,那麼您也可以在這些情況下避免註冊服務工作站。

您應繼續將服務工作者視為漸進式增強功能,在滿足所有必要條件後,將其新增至網路應用程式,並讓服務工作者為使用者體驗和整體載入效能帶來正面影響。

評估一切

如要判斷發布服務工作者對使用者體驗的影響是正面還是負面,唯一的方法就是進行實驗並評估結果。

具體設定有意義的評估方式取決於您使用的數據分析供應商,以及您通常在部署設定中進行實驗的方式。這份個案研究詳細說明瞭使用 Google Analytics 收集指標的方法,並根據在 Google I/O 網站應用程式中使用服務工作者的經驗。

非目標

雖然許多網頁開發人員會將 Service Worker 與漸進式網頁應用程式建立關聯,但建構「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 搜尋團隊已重新評估現有服務 worker 架構的優點和取捨。上述的服務工作者已淘汰。隨著 Google 搜尋網頁基礎架構的演進,團隊可能會重新檢視 Service Worker 設計。