說明發布內容、評估影響的方式,以及所做的取捨。
發布日期:2019 年 6 月 20 日
在 Google 上搜尋任何主題,系統都會立即顯示有意義的相關結果,讓您一目瞭然。您可能沒注意到的是,在特定情況下,這個搜尋結果網頁是由稱為服務工作人員的強大網路技術提供。
為 Google 搜尋推出 Service Worker 支援功能,同時不對效能造成負面影響,這項工作需要數十位工程師跨多個團隊合作。本文將說明發布內容、成效評估方式,以及所做的取捨。
探索 Service Worker 的主要原因
為網站應用程式新增 Service Worker,就像對網站進行任何架構變更一樣,都應以明確的目標為前提。對 Google 搜尋團隊而言,新增 Service Worker 值得探索的原因有幾個。
搜尋結果快取功能有限
Google 搜尋團隊發現,使用者經常在短時間內多次搜尋相同字詞。搜尋團隊不想為了取得可能相同的結果而觸發新的後端要求,因此決定善用快取功能,在本地滿足這些重複要求。
新鮮度非常重要,有時使用者會重複搜尋相同字詞,因為主題不斷演變,他們希望看到最新的結果。搜尋團隊可透過服務工作人員實作精細的邏輯,控管本機快取搜尋結果的生命週期,並精確平衡速度與新鮮度,提供最符合使用者需求的服務。
有意義的離線體驗
此外,Google 搜尋團隊也希望提供有意義的離線體驗。使用者想瞭解某個主題時,會希望直接前往 Google 搜尋頁面並開始搜尋,不必擔心網路連線是否正常。
如果沒有 Service Worker,使用者在離線時造訪 Google 搜尋頁面,只會看到瀏覽器的標準網路錯誤頁面,而且必須記得在連線恢復後返回並重試。有了 Service Worker,就能提供自訂的離線 HTML 回應,並允許使用者立即輸入搜尋查詢。

必須連上網際網路才能取得結果,但服務工作人員可延後搜尋作業,並在裝置使用背景同步 API 重新連線後,立即將搜尋要求傳送至 Google 伺服器。
更聰明的 JavaScript 快取和提供方式
另一個動機是最佳化模組化 JavaScript 程式碼的快取和載入作業,這些程式碼支援搜尋結果網頁上的各種功能。JavaScript 組合提供許多優點,在沒有 Service Worker 參與的情況下,這些優點很有意義,因此搜尋團隊不想完全停止組合。
搜尋團隊認為,他們可以運用 Service Worker 在執行階段對 JavaScript 細微區塊進行版本控制和快取,減少快取流失量,並確保日後重複使用的 JavaScript 能有效率地快取。服務工作人員中的邏輯可以分析包含多個 JavaScript 模組的套件外送 HTTP 要求,並將多個本機快取的模組拼湊在一起,以滿足要求,盡可能「取消套件」。這樣做可節省使用者頻寬,並提升整體回應速度。
使用服務工作人員提供的快取 JavaScript 也有助於提升效能:在 Chrome 中,系統會儲存並重複使用該 JavaScript 的剖析位元組程式碼表示法,因此在網頁上執行 JavaScript 時,需要完成的工作較少。
挑戰與解決方案
以下是為達成團隊既定目標,必須克服的幾項障礙。雖然其中有些挑戰是 Google 搜尋特有的,但許多挑戰適用於考慮部署 Service Worker 的各種網站。
問題:Service worker 負荷過高
最大的挑戰,也是在 Google 搜尋上推出 Service Worker 的唯一阻礙,就是確保 Service Worker 不會執行任何可能增加使用者感受到的延遲時間。Google 搜尋非常重視效能,過去如果新功能會為特定使用者群增加數十毫秒的延遲時間,Google 就會封鎖該功能的發布。
團隊在最早的實驗中開始收集成效資料時,就發現會有問題。為搜尋結果網頁導覽要求傳回的 HTML 是動態的,且會根據需要在 Google 搜尋網頁伺服器上執行的邏輯而大幅變動。服務工作站目前無法複製這項邏輯並立即傳回快取的 HTML,最多只能將導覽要求傳遞至後端網路伺服器,這需要網路要求。
如果沒有 Service Worker,使用者瀏覽時會立即發出這項網路要求。註冊服務工作人員時,一律需要啟動服務工作人員,並讓服務工作人員有機會執行 fetch 事件處理常式,即使這些擷取處理常式除了前往網路外,不會執行任何其他動作也一樣。啟動及執行 Service Worker 程式碼所需的時間,完全是每次導覽額外增加的負擔:

這會導致服務工作人員實作的延遲時間過長,無法抵銷其他優點。此外,團隊在實際裝置上測量服務工作人員的啟動時間後發現,啟動時間的分布範圍很廣,部分低階行動裝置啟動服務工作人員所需的時間,幾乎與要求結果網頁 HTML 的網路要求時間相同。
解決方法:使用導覽預先載入
Google 搜尋團隊能順利推出 Service Worker,最關鍵的功能就是導覽預先載入。如果服務工作人員需要使用網路的回應來滿足導覽要求,使用導覽預先載入功能是提升效能的關鍵。這會向瀏覽器提供提示,在啟動 Service Worker 的同時,立即開始發出導覽要求:

只要服務工作站啟動所需的時間少於從網路取得回應所需的時間,服務工作站就不會造成任何延遲負擔。
搜尋團隊也必須避免在低階行動裝置上使用 Service Worker,因為這類裝置的 Service Worker 啟動時間可能會超過瀏覽要求。由於沒有明確的「低階」裝置定義,因此他們想出檢查裝置上安裝的總 RAM 的啟發式方法。記憶體容量低於 2 GB 的裝置屬於低階裝置類別,服務工作站的啟動時間會無法接受。
此外,您也必須考量可用儲存空間,因為要快取供日後使用的完整資源集,可能需要數 MB 的空間。Google 搜尋頁面可透過 navigator.storage 介面預先判斷,嘗試快取資料時是否可能因儲存空間配額失敗而導致作業失敗。
這樣一來,搜尋團隊就能根據多項條件,判斷是否要使用 Service Worker:如果使用者透過支援導覽預先載入的瀏覽器前往 Google 搜尋頁面,且 RAM 至少有 2 GB,並有足夠的可用儲存空間,系統就會註冊 Service Worker。如果不符合上述條件,瀏覽器或裝置就不會使用 Service Worker,但仍會提供與以往相同的 Google 搜尋體驗。
選擇性註冊的其中一項好處,就是能夠運送更小、更有效率的 Service Worker。以相當新式的瀏覽器為目標來執行 Service Worker 程式碼,可避免舊版瀏覽器轉譯和 polyfill 的負擔。這項做法最終從服務工作人員實作的總大小中,刪除了約 8 KB 的未壓縮 JavaScript 程式碼。
問題:Service Worker 範圍
搜尋團隊進行足夠的延遲實驗,並確信使用導覽預先載入功能,可為他們提供延遲中立的服務工作人員使用路徑後,一些實際問題開始浮上檯面。其中一個問題與服務工作人員的範圍規則有關。 Service Worker 的範圍決定了它可能控管的網頁。
範圍設定是根據網址路徑前置字串運作。如果網域代管的是單一網頁應用程式,這就不會是問題,因為您通常只會使用範圍最大的 / 服務工作人員,這類服務工作人員可以控管網域下的任何網頁。但 Google 搜尋的網址結構稍微複雜一些。
如果服務工作人員獲得 / 的最大範圍,最終將能控管 www.google.com (或區域對應項目) 下代管的任何網頁,而該網域下的網址與 Google 搜尋無關。更合理的限制範圍是 /search,至少可以排除與搜尋結果完全無關的網址。
很遺憾,即使是 /search 網址路徑,也會在不同類型的 Google 搜尋結果之間共用,並由網址查詢參數決定顯示的特定搜尋結果類型。其中有些版本使用的程式碼庫與傳統網頁搜尋結果頁面完全不同。舉例來說,圖片搜尋和購物搜尋都是在 /search URL 路徑下放送,但查詢參數不同,且這兩個介面都尚未準備好推出自己的 Service Worker 體驗。
解決方案:建立分派和轉送架構
雖然部分提案允許使用比網址路徑前置字串更強大的功能來判斷 Service Worker 範圍,但 Google 搜尋團隊仍受限於部署 Service Worker,無法為控管的網頁子集執行任何動作。
為解決這個問題,Google 搜尋團隊建構了專屬的調度與路徑架構,可設定檢查用戶端網頁的查詢參數等條件,並根據這些條件判斷要採用哪個特定程式碼路徑。這個系統並非採用硬式編碼規則,而是具備彈性,可讓共用網址空間的團隊 (例如圖片搜尋和購物搜尋) 在日後決定實作時,加入自己的 Service Worker 邏輯。
問題:個人化結果和指標
使用者可以透過 Google 帳戶登入 Google 搜尋,系統可能會根據特定帳戶資料自訂搜尋結果體驗。系統會透過特定的瀏覽器 Cookie 識別登入的使用者,這是歷史悠久且廣受支援的標準。
不過,使用瀏覽器 Cookie 的缺點是,Cookie 不會顯示在 Service Worker 內,而且無法自動檢查 Cookie 值,確保這些值不會因為使用者登出或切換帳戶而變更。(目前正在努力讓服務工作人員存取 Cookie,但截至本文撰寫時,這項做法仍處於實驗階段,尚未獲得廣泛支援)。
如果服務工作站檢視的目前登入使用者與實際登入 Google 搜尋網頁介面的使用者不符,可能會導致搜尋結果個人化設定錯誤,或是指標和記錄歸因錯誤。對 Google 搜尋團隊來說,上述任何失敗情況都是嚴重問題。
解決方法:使用 postMessage 傳送 Cookie
Google 搜尋團隊並未等待實驗性 API 發布,直接存取服務工作站內的瀏覽器 Cookie,而是採用權宜之計:每當載入服務工作站控制的網頁時,網頁會讀取相關 Cookie,並使用 postMessage() 將 Cookie 傳送至服務工作站。
接著,服務工作人員會比對目前的 Cookie 值與預期值,如果兩者不符,就會採取步驟,從儲存空間清除所有使用者專屬資料,並重新載入搜尋結果網頁,不會顯示任何錯誤的個人化內容。
服務工作人員將項目重設為基準的具體步驟,是 Google 搜尋特有的需求,但對於處理以瀏覽器 Cookie 為鍵的個人化資料的其他開發人員來說,一般方法可能很有用。
問題:實驗和動態性
如前所述,Google 搜尋團隊非常仰賴在實際運作環境中進行實驗,並在預設啟用新程式碼和功能前,測試這些項目在現實世界中的影響。如果靜態服務工作人員大量依賴快取資料,這可能會有點困難,因為讓使用者加入及退出實驗通常需要與後端伺服器通訊。
解決方案:動態產生的 Service Worker 指令碼
最後團隊選擇使用動態產生的服務工作人員指令碼,由網路伺服器為每位使用者自訂,而不是預先產生單一靜態服務工作人員指令碼。影響服務工作站行為或一般網路要求的實驗相關資訊,會直接納入這個自訂服務工作站指令碼。如要變更使用者的有效體驗組合,請結合使用傳統技術 (例如瀏覽器 Cookie),以及在已註冊的 Service Worker 網址中提供更新的程式碼。
使用動態產生的 Service Worker 指令碼,也能在 Service Worker 實作項目發生需要避免的重大錯誤時,更輕鬆地提供緊急應變措施。動態服務工作人員回應可能是無運算實作,這會為部分或所有目前使用者有效停用服務工作人員。
問題:協調更新
在部署任何實際服務工作站時,最艱鉅的挑戰之一,就是在避免使用網路而改用快取,同時確保現有使用者在部署至正式環境後,能盡快取得重要更新和變更之間,做出合理的取捨。適當的平衡取決於許多因素:
- 無論您的網頁應用程式是長期執行的單頁應用程式,使用者都會無限期保持開啟,不會瀏覽新網頁。
- 後端網頁伺服器更新的部署週期。
- 一般使用者是否能接受使用稍微過時的網頁應用程式版本,還是以最新版本為優先。
在實驗服務工作人員時,Google 搜尋團隊確保實驗會在多項排定的後端更新中持續進行,以確保指標和使用者體驗更貼近回訪使用者在現實世界中看到的內容。
解決方案:平衡即時性和快取使用率
Google 搜尋團隊測試了多種設定選項後,發現下列設定可適當平衡新舊資料和快取使用率。
服務工作人員指令碼網址會隨附 Cache-Control: private, max-age=1500 (1500 秒或 25 分鐘) 回應標頭,並向 updateViaCache 註冊,且值設為「all」,確保標頭受到尊重。如您所想,Google 搜尋網頁後端是一組遍布全球的大型伺服器,需要盡可能接近 100% 的正常運作時間。部署會影響 Service Worker 指令碼內容的變更時,系統會以輪流方式進行。
如果使用者連上已更新的後端,然後快速前往另一個後端,但該後端尚未收到更新的 Service Worker,使用者就會在不同版本之間來回切換多次。因此,如果上次檢查後已過了 25 分鐘,才要求瀏覽器檢查更新的指令碼,不會有顯著的缺點。選擇啟用這項行為的優點是,可大幅減少動態產生 Service Worker 指令碼的端點收到的流量。
此外,服務工作人員指令碼的 HTTP 回應會設定 ETag 標頭,確保在經過 25 分鐘後進行更新檢查時,如果部署的服務工作人員在這段期間沒有任何更新,伺服器可以有效率地傳回 HTTP 304 回應。
雖然 Google 搜尋網頁應用程式中的部分互動會使用單頁應用程式風格的導覽 (即透過 History API),但 Google 搜尋基本上是使用「實際」導覽的傳統網頁應用程式。當團隊決定使用兩種選項來加速 Service Worker 更新生命週期時,這項功能就會派上用場:clients.claim() 和 skipWaiting()。在 Google 搜尋介面中點選項目時,通常會導向新的 HTML 文件。呼叫 skipWaiting 可確保更新的 Service Worker 在安裝後,有機會立即處理這些新的導覽要求。同樣地,呼叫 clients.claim() 表示更新後的服務工作人員有機會在服務工作人員啟動後,開始控管任何不受控管的 Google 搜尋網頁。
Google 搜尋採用的方法不一定適用於所有人,這是經過仔細的 A/B 測試,找出最適合自己的各種放送選項組合後,才得出的結果。如果開發人員的後端基礎架構允許他們更快速地部署更新,他們可能會希望瀏覽器盡可能頻繁地檢查更新的 Service Worker 指令碼,方法是一律忽略 HTTP 快取。如果您要建構單頁應用程式,使用者可能會長時間開啟,則使用 skipWaiting() 可能不是正確的選擇,因為如果允許新的 Service Worker 在長期存在的用戶端中啟動,可能會遇到快取不一致的問題。
重點回顧
根據預設,服務工作人員不會對效能造成影響
在網頁應用程式中新增 Service Worker,表示要插入額外的 JavaScript 片段,且必須先載入及執行該片段,網頁應用程式才能取得要求的相關回應。如果這些回應最終來自本機快取而非網路,相較於快取優先策略帶來的效能提升,執行 Service Worker 的負擔通常微不足道。但如果您知道服務工作人員在處理導覽要求時,一律必須查詢網路,使用導覽預先載入功能就能大幅提升效能。
服務工作人員 (仍然!) 是漸進式強化功能
與一年前相比,現在的 Service Worker 支援情況好得多。所有新式瀏覽器現在都至少支援 Service Worker,但很遺憾的是,部分進階 Service Worker 功能 (例如背景同步和導覽預先載入) 並未全面推出。檢查您需要的特定功能子集,並僅在這些功能存在時註冊 Service Worker,仍是合理的做法。
同樣地,如果您已在實際環境中執行實驗,並發現低階裝置因服務工作站的額外負荷而效能不佳,也可以在這些情況下避免註冊服務工作站。
您應繼續將服務工作人員視為漸進式強化,在滿足所有先決條件,且服務工作人員能為使用者體驗和整體載入效能帶來正面影響時,再將其新增至 Web 應用程式。
評估一切
如要判斷運送 Service Worker 對使用者體驗造成正面或負面影響,唯一的方法就是進行實驗並評估結果。
設定有意義的評估指標時,具體做法取決於您使用的 Analytics 供應商,以及您在部署設定中進行實驗的通常方式。其中一種方法是使用 Google Analytics 收集指標,這份研究案例詳細說明瞭在 Google I/O 網頁應用程式中使用 Service Worker 的體驗。
非目標
雖然網頁開發社群中的許多人會將 Service Worker 與漸進式網頁應用程式建立關聯,但團隊的初步目標並非建構「Google 搜尋漸進式網頁應用程式」。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 即將淘汰。隨著 Google 搜尋網頁基礎架構的演進,團隊可能會重新檢視 Service Worker 設計。