ข้ามไปยังเนื้อหา

Lifecycle ของ Service Worker

service worker ทุกตัวจะเคลื่อนผ่าน state machine ที่ browser กำหนดไว้ตายตัว การเข้าใจ lifecycle นี้เป็นเรื่องจำเป็น — ข้ามไปแล้วการอัปเดตของคุณจะ activate ไม่สำเร็จแบบเงียบ ๆ หรือคุณทำให้หน้าเว็บที่ยังรัน worker ตัวเก่าอยู่พังได้

stateDiagram-v2
  [*] --> Parsed : browser parses sw.js
  Parsed --> Installing : install event fires
  Installing --> Installed : installEvent.waitUntil() resolves
  Installing --> Redundant : waitUntil() rejects
  Installed --> Activating : no old SW active, or skipWaiting()
  Installed --> Waiting : old SW still controlling pages
  Waiting --> Activating : all old-SW tabs closed, or skipWaiting()
  Activating --> Activated : activateEvent.waitUntil() resolves
  Activating --> Redundant : waitUntil() rejects
  Activated --> Redundant : replaced by a newer SW
  Waiting --> Redundant : replaced by a newer SW
state machine lifecycle ของ service worker

install event จะ fire ทันทีที่ browser parse และดาวน์โหลดไฟล์ service worker ของคุณเสร็จ นี่เป็นจังหวะที่เหมาะที่สุดในการ pre-cache ทรัพยากรที่แอปของคุณต้องใช้สำหรับการใช้งานออฟไลน์

const CACHE_NAME = 'v1';
const PRECACHE_URLS = ['/', '/styles.css', '/app.js'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
);
});

event.waitUntil() รับ Promise เข้ามา ถ้า Promise resolve SW จะย้ายไปสถานะ Installed ถ้า Promise reject (เช่น เกิด network error ระหว่าง pre-cache) SW จะย้ายไป Redundant และถูกทิ้ง — ไม่มีอะไรพังสำหรับผู้ใช้ SW ตัวเก่ายังคงทำงานต่อไป

หลังจากติดตั้งแล้ว SW ตัวใหม่จะนั่งรออยู่ในสถานะ Waiting ถ้ายังมีแท็บใด ๆ ที่ถูกควบคุมโดยเวอร์ชันก่อนหน้าอยู่ browser จะไม่ activate SW ตัวใหม่จนกว่าทุกแท็บที่รันตัวเก่าจะถูกปิด (หรือ navigate ออกไป)

หากต้องการข้ามช่วงรอด้วยโค้ด ให้เรียก self.skipWaiting() ภายใน handler ของ install:

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => cache.addAll(['/'])).then(() => self.skipWaiting())
);
});

เมื่อ SW ตัวเก่าหายไปแล้ว (หรือมีการเรียก skipWaiting()) SW ตัวใหม่จะ fire activate event ของตัวเอง นี่เป็นจังหวะที่เหมาะในการ ล้าง cache เก่า เพื่อไม่ให้ asset ที่ล้าสมัยกินพื้นที่จัดเก็บไปเรื่อย ๆ ไม่รู้จบ

const CURRENT_CACHES = ['v2'];
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => !CURRENT_CACHES.includes(name))
.map((name) => caches.delete(name))
)
).then(() => self.clients.claim())
);
});

self.clients.claim() ทำให้ SW ที่เพิ่ง activate เข้าควบคุมทุกหน้าที่เปิดอยู่ได้ทันที โดยไม่ต้องรอ navigation ครั้งถัดไป

สถานะเกิดอะไรขึ้นสิ่งที่คุณทำ
InstallingSW กำลังถูกตั้งค่าpre-cache asset ใน install
WaitingSW ตัวใหม่พร้อมแล้วแต่ถูกถ่วงไว้เรียก skipWaiting() ได้ตามต้องการ
ActivatingSW ตัวใหม่กำลังเข้ารับช่วงต่อลบ cache เก่าใน activate
ActivatedSW ควบคุมเต็มที่แล้วเสิร์ฟ request ผ่าน fetch event
RedundantSW ถูกแทนที่หรือล้มเหลวไม่ต้องทำอะไร — browser จะกำจัดให้เอง
ตัวเลือกBenefitCost
ใช้ skipWaiting() + clients.claim()ผู้ใช้ได้ version ใหม่ทันทีโดยไม่ต้องปิด tab ทั้งหมดเสี่ยง breaking change ถ้า version เก่ากับใหม่ไม่ compatible กันระหว่างที่หน้าเว็บยังเปิดอยู่
ปล่อยให้รอ default lifecycle (waiting จนกว่า tab ทั้งหมดจะปิด)ปลอดภัยกว่า ไม่มี version mismatch ระหว่าง client กับ serverผู้ใช้อาจติดอยู่กับ version เก่านานถ้าไม่ปิด tab เอง
  • ลืมว่า install event ต้อง resolve สำเร็จก่อนเข้า waiting — ถ้า cache.addAll() fail ทั้ง install จะ fail เงียบๆ
  • เรียก skipWaiting() โดยไม่คู่กับ versioned cache name ทำให้ client เปลี่ยนไปใช้ service worker ใหม่แต่ยังอ่าน cache เก่าที่ไม่ตรงกัน
  • ไม่ทดสอบ flow “หลาย tab เปิดพร้อมกัน” ทำให้พลาด edge case ที่ worker เก่ายังคุม tab บางอันอยู่ระหว่าง transition

💡 ตัวอย่างจากของจริง

Google Docs — คุม lifecycle อย่างระมัดระวังเพื่อไม่ให้ผู้ใช้ที่กำลังแก้เอกสารอยู่โดน service worker ใหม่ตัดการทำงานกลางคัน

YouTube — ใช้ state machine ของ lifecycle ร่วมกับ UI แจ้งเตือน “มีเวอร์ชันใหม่” แทนที่จะ force reload ผู้ใช้แบบไม่บอกกล่าว

จะเกิดอะไรขึ้นถ้า Promise ที่ส่งให้ event.waitUntil() ใน install handler reject?
ทำไม service worker ตัวใหม่จึงนั่งรออยู่ในสถานะ Waiting?
self.clients.claim() ใน activate handler มีจุดประสงค์อะไร?
สถานะใดใน lifecycle เป็นจังหวะที่เหมาะสมในการลบ cache ที่ล้าสมัยจาก SW เวอร์ชันก่อน ๆ?