Service Worker 筆記

Created: 2025/8/15
Updated: 2025/8/17

記錄了一些關於service worker的特性和一些執行流程


1. 基本概念

Service Worker (SW) 是一個在瀏覽器背景執行的腳本,作為網頁與瀏覽器之間的代理服務器。

核心特性

  • 獨立線程運行:不會阻塞主線程 UI 操作
  • 事件驅動模式:基於事件監聽和響應
  • HTTPS 限制:出於安全考量,僅在 HTTPS 或 localhost 環境下工作
  • 作用域控制:只能控制其註冊路徑下的頁面
  • 無 DOM 訪問:無法直接操作 DOM,需透過 postMessage 通信
  • 可程式化網路代理:可攔截和處理網路請求

核心概念架構

┌─────────────────────────────────────────────────────────┐
│                     Browser Process                      │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  ┌──────────────┐        ┌──────────────────────┐      │
│  │   Web Page   │◄──────►│   Service Worker     │      │
│  │   (主線程)    │        │    (獨立線程)         │      │
│  └──────────────┘        └──────────────────────┘      │
│         ▲                          │                     │
│         │                          │ Intercept           │
│         │                          ▼                     │
│  ┌──────────────┐        ┌──────────────────────┐      │
│  │    Cache     │◄───────│   Network Request   │      │
│  │   Storage    │        │                      │      │
│  └──────────────┘        └──────────────────────┘      │
└─────────────────────────────────────────────────────────┘

2. 生命週期

完整生命週期流程

     ┌─────────────┐
     │   未註冊     │
     └──────┬──────┘
            │ navigator.serviceWorker.register()
            ▼
     ┌─────────────┐
     │   Parsing    │ ← SW 腳本解析
     └──────┬──────┘
            │ 解析成功
            ▼
     ┌─────────────┐
     │  Installing  │ ← install event 觸發
     └──────┬──────┘
            │ 安裝完成
            ▼
     ┌─────────────┐
     │   Waiting    │ ← 等待舊版本退出
     └──────┬──────┘
            │ 舊版本退出或 skipWaiting()
            ▼
     ┌─────────────┐
     │  Activating  │ ← activate event 觸發
     └──────┬──────┘
            │ 激活完成
            ▼
     ┌─────────────┐
     │   Activated  │ ← 開始控制頁面
     │   (Active)   │ ← fetch/message/sync/push events
     └──────┬──────┘
            │ 新版本註冊
            ▼
     ┌─────────────┐
     │  Redundant   │ ← 被新版本取代
     └─────────────┘

生命週期階段說明

  1. Registration (註冊)

    • 時機:頁面 JavaScript 執行時
    • 觸發:navigator.serviceWorker.register()
    • 作用:下載並解析 SW 腳本
  2. Installation (安裝)

    • 時機:首次註冊或檢測到新版本
    • 事件:install event
    • 作用:預緩存靜態資源
  3. Waiting (等待)

    • 時機:新版本安裝完成但舊版本仍在控制頁面
    • 作用:等待所有使用舊版本的頁面關閉
    • 跳過:self.skipWaiting() 可強制激活
  4. Activation (激活)

    • 時機:沒有頁面使用舊版本或調用 skipWaiting()
    • 事件:activate event
    • 作用:清理舊緩存,接管頁面控制權
  5. Active (運行中)

    • 時機:激活完成後
    • 事件:fetch, message, sync, push 等
    • 作用:攔截請求、推送通知、背景同步

3. Service Worker 介入時機與場景分析

兩個關鍵介入時機

時機 1:首次註冊(背景安裝)

前一步驟:JavaScript 執行階段
├─ DOM 已經構建完成
├─ <script> 標籤正在執行
├─ 頁面的主要 JavaScript 代碼運行中
└─ 此時頁面已經可以與用戶互動
         ↓
         ↓ 如何介入:開發者在 JS 中調用註冊 API
         ↓
┌─────────────────────────────────────────────┐
│ 🔴 Service Worker 註冊點                      │
│                                              │
│ // 通常在 DOMContentLoaded 或 load 事件中     │
│ navigator.serviceWorker.register('/sw.js')   │
│                                              │
│ 介入影響:                                    │
│ • 下載並解析 sw.js(背景執行,不阻塞頁面)      │
│ • 觸發 install 事件                          │
│ • 建立緩存存儲                               │
│ • 預載入關鍵資源                             │
└─────────────────────────────────────────────┘
         ↓
         ↓ 註冊後的影響
         ↓
最後步驟:頁面完成載入(Load Event)
├─ Service Worker 在背景安裝中
├─ 當前頁面不受 SW 控制(需刷新)
├─ 頁面正常完成載入流程
└─ SW 狀態:Installing → Installed → Waiting
  • 前一步驟:瀏覽器正在執行頁面的 JavaScript,此時 DOM 已經準備好,可以操作頁面元素
  • 介入點:JavaScript 執行階段
  • 影響:背景下載並安裝,不阻塞當前頁面
  • 結果:需刷新頁面才能讓 SW 控制

時機 2:控制頁面後(攔截請求)

前一步驟:用戶導航動作
├─ 用戶在地址欄輸入 URL
├─ 用戶點擊頁面連結
├─ 用戶刷新頁面(F5)
└─ JavaScript 觸發導航(location.href)
         ↓
         ↓ 如何介入:瀏覽器自動將請求轉發給 SW
         ↓
┌─────────────────────────────────────────────┐
│ 🔴 Service Worker 攔截點                      │
│                                              │
│ self.addEventListener('fetch', event => {    │
│   // 在網路請求發出前攔截,此時可以決定:       │
│   // 1. 返回緩存(超快)                      │
│   // 2. 發送網路請求                         │
│   // 3. 返回自定義響應                        │
│ })                                           │
│                                              │
│ 介入影響:                                    │
│ • 完全繞過 DNS 查詢                          │
│ • 跳過 TCP/TLS 握手                         │
│ • 不需要發送 HTTP 請求                       │
│ • 直接從本地緩存返回                         │
└─────────────────────────────────────────────┘
         ↓
         ↓ 攔截後的處理
         ↓
最後步驟:返回響應 → 頁面渲染
├─ 緩存命中:立即返回資源(<10ms)
├─ 緩存未中:降級到網路請求
├─ 混合策略:返回緩存同時更新
└─ 頁面接收響應後正常解析渲染
  • 前一步驟:用戶觸發導航,瀏覽器準備發起網路請求
  • 介入點:瀏覽器準備發起網路請求時
  • 影響:完全繞過 DNS/TCP/TLS,直接從緩存返回
  • 結果:載入速度提升 80-90%

三種典型場景對比

場景對比表

載入階段無 SW首次訪問(註冊 SW)SW 已控制
DNS 查詢50-100ms50-100ms0ms
TCP 握手50-100ms50-100ms0ms
TLS 握手100-200ms100-200ms0ms
HTTP 請求100-200ms100-200ms0ms
等待響應100-500ms100-500ms1-10ms
下載內容200-1000ms200-1000ms5-20ms
總時間600-2100ms600-2100ms6-30ms
SW 狀態背景安裝中完全控制

詳細流程對比

瀏覽器Service Worker網路緩存場景 1: 無 SW(傳統流程)場景 2: 首次訪問(註冊 SW)場景 3: SW 已控制完整網路請求返回資源正常網路請求背景註冊 SW預緩存資源請求被攔截檢查緩存立即返回瀏覽器Service Worker網路緩存

4. 事件監聽器

常見的事件分類

Service Worker 提供多種事件類型來處理不同的應用場景,以下是常見事件的分類和使用說明:

  • 生命週期事件:install, activate
  • 功能性事件:fetch, push, sync, periodicsync
  • 通知事件:notificationclick, notificationclose
  • 通信事件:message, messageerror
  • 實驗性事件:contentdelete, backgroundfetch
事件名稱使用頻率主要用途說明
install初始安裝SW 首次安裝時觸發,用於快取資源初始化
activate版本更新SW 啟用時觸發,用於清理舊快取和版本管理
fetch網路代理攔截所有網路請求,實現快取策略和離線功能
sync背景同步網路恢復時同步離線期間的資料
periodicsync定期同步定期在背景執行資料同步(實驗性功能)
push推播通知接收伺服器推送的通知訊息
notificationclick通知互動處理用戶點擊通知的行為
notificationclose通知關閉處理用戶關閉通知的行為
message訊息傳遞與主執行緒或其他 SW 進行雙向通信
messageerror錯誤處理處理訊息傳遞過程中的錯誤
contentdelete內容刪除處理快取內容的刪除請求(實驗性)
backgroundfetch背景下載處理大型檔案的背景下載(實驗性)

使用優先級建議

  1. 必須實作(高頻率):installactivatefetch
  2. 推薦實作(中頻率):messagepushsync
  3. 選擇性實作(低頻率):通知相關事件、實驗性事件

注意事項:

  • 生命週期事件:確保正確的版本管理和快取策略
  • 功能性事件:fetch 是核心功能,需謹慎處理以避免影響效能
  • 通知事件:需要用戶授權才能使用
  • 實驗性事件:瀏覽器支援度有限,使用前需檢查相容性

核心事件實作

1. 生命週期事件

// install - 預緩存資源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js',
        '/offline.html'
      ]);
    })
  );
  // 可選:跳過等待
  self.skipWaiting();
});
 
// activate - 清理舊緩存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== 'v1') {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  // 立即控制所有客戶端
  return self.clients.claim();
});

2. 功能性事件

// fetch - 攔截網路請求
self.addEventListener('fetch', event => {
  const { request } = event;
  
  // 根據資源類型處理
  if (request.destination === 'document') {
    event.respondWith(handlePageRequest(request));
  } else if (request.url.includes('/api/')) {
    event.respondWith(handleApiRequest(request));
  } else {
    event.respondWith(
      caches.match(request).then(response => {
        return response || fetch(request);
      })
    );
  }
});
 
// push - 推送通知
self.addEventListener('push', event => {
  const options = {
    body: event.data ? event.data.text() : 'New notification',
    icon: '/icon-192.png',
    badge: '/badge-72.png'
  };
  
  event.waitUntil(
    self.registration.showNotification('Push Notification', options)
  );
});
 
// sync - 背景同步
self.addEventListener('sync', event => {
  if (event.tag === 'sync-messages') {
    event.waitUntil(syncMessages());
  }
});

5. 緩存策略

根據我們的需求,我們能選擇各種不同的緩存策略,sevice worker有三種策略: Cache First、Stale-While-Revalidate以及Network First

主要策略實作

1. Cache First (緩存優先)

適用:靜態資源(CSS, JS, 圖片)

async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  
  const response = await fetch(request);
  const cache = await caches.open('static-v1');
  cache.put(request, response.clone());
  return response;
}

2. Network First (網路優先)

適用:API 請求、動態內容

async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open('dynamic-v1');
    cache.put(request, response.clone());
    return response;
  } catch (error) {
    return caches.match(request);
  }
}

3. Stale While Revalidate

適用:頻繁更新但可接受舊版本的內容

async function staleWhileRevalidate(request) {
  const cached = await caches.match(request);
  
  const fetchPromise = fetch(request).then(response => {
    const cache = await caches.open('revalidate-v1');
    cache.put(request, response.clone());
    return response;
  });
  
  return cached || fetchPromise;
}

6. 實作指南

基本實作步驟

// 1. 在主頁面註冊 SW
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('SW registered'))
      .catch(err => console.log('SW registration failed'));
  });
}
 
// 2. 在 sw.js 實作基本功能
const CACHE_NAME = 'app-v1';
const urlsToCache = [
  '/',
  '/styles.css',
  '/script.js'
];
 
// 安裝並預緩存
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});
 
// 清理舊版本
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
 
// 處理請求
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

最佳實踐

  1. 版本管理
const VERSION = '1.0.0';
const CACHE_NAME = `app-v${VERSION}`;
  1. 緩存大小控制
async function limitCacheSize(name, size) {
  const cache = await caches.open(name);
  const keys = await cache.keys();
  if (keys.length > size) {
    cache.delete(keys[0]);
  }
}
  1. 更新通知
self.addEventListener('activate', event => {
  event.waitUntil(
    self.clients.matchAll().then(clients => {
      clients.forEach(client => {
        client.postMessage({ type: 'SW_UPDATED' });
      });
    })
  );
});

7. 常見問題

Q1: Service Worker 不更新?

  • 更改 sw.js 檔案內容(即使是註釋)
  • 使用 registration.update() 強制更新
  • 在 Chrome DevTools 勾選 "Update on reload"

Q2: 緩存過多佔用空間?

  • 設定緩存過期時間
  • 限制緩存數量
  • 定期清理舊版本緩存

Q3: 如何偵錯?

Chrome DevTools
├── Application Tab
│   ├── Service Workers (狀態查看)
│   ├── Cache Storage (緩存檢視)
│   └── Clear Storage (清理)
├── Network Tab
│   └── Check "Offline" (離線測試)
└── Console
    └── SW Logs (除錯訊息)

8. 總結

Service Worker 是實現 Progressive Web App 的核心技術,通過理解其生命週期、掌握 API 使用、選擇適當的緩存策略,可以顯著提升 Web 應用的效能和用戶體驗。

關鍵要點:

  1. 理解執行時機:從註冊到控制頁面的完整流程
  2. 選擇緩存策略:根據資源類型選擇合適策略
  3. 管理版本更新:確保用戶獲得最新版本
  4. 監控與優化:持續監控緩存命中率和載入效能

Service Worker 讓 Web 應用擁有了原生應用般的體驗,是現代 Web 開發不可或缺的技術。