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

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

แอปที่แก้ไขเอกสารหรือฟอร์มยาวๆ แบบ real-time มักเลือก lifecycle เริ่มต้น (waiting จนกว่าทุกแท็บจะปิด) เพื่อไม่ให้ผู้ใช้ที่กำลังพิมพ์งานอยู่โดน service worker ใหม่ตัดการทำงานกลางคัน ส่วนแอปที่เน้นให้ผู้ใช้ได้ฟีเจอร์ใหม่เร็วมักผูก UI แจ้งเตือน “มีเวอร์ชันใหม่” เข้ากับ state machine ของ lifecycle แทนการ force reload แบบไม่บอกกล่าว — สองแนวทางนี้คือ trade-off เดียวกับตารางด้านบน เลือกตามลักษณะการใช้งานจริงของแอป

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