การอัปเดต Service Worker
การอัปเดต service worker
หัวข้อที่มีชื่อว่า “การอัปเดต service worker”การ deploy PWA เวอร์ชันใหม่ก็คือการ deploy service worker ตัวใหม่ การเข้าใจว่า browser ตรวจจับและนำการอัปเดตนั้นมาใช้อย่างไร — และจะสื่อสารกับผู้ใช้อย่างไร — ช่วยป้องกันบั๊กที่แอบแฝงและความประหลาดใจเรื่อง asset ที่ล้าสมัย
browser ตรวจจับการอัปเดตอย่างไร
หัวข้อที่มีชื่อว่า “browser ตรวจจับการอัปเดตอย่างไร”browser จะตรวจไฟล์ service worker ของคุณว่ามีการเปลี่ยนแปลงหรือไม่ในทุกครั้งที่โหลดหน้า (และทุกครั้งที่คุณเรียก registration.update() เอง) โดยเปรียบเทียบไฟล์ที่ดาวน์โหลดมาใหม่กับ SW ที่ติดตั้งอยู่ปัจจุบันแบบ byte-for-byte แม้ความต่างเพียง ไบต์เดียว — string เวอร์ชันของ cache ที่เปลี่ยนไป, comment ใหม่, การปรับ whitespace — ก็ทริกเกอร์ flow การอัปเดต:
- SW ตัวใหม่ถูกดาวน์โหลดและ parse
- SW ตัวใหม่เข้าสู่เฟส
install - ถ้าการติดตั้งสำเร็จ SW ตัวใหม่จะนั่งอยู่ใน Waiting — ไม่สามารถ activate ได้ขณะที่ SW ตัวเก่ายังควบคุมแท็บที่เปิดอยู่
ปัญหา waiting worker
หัวข้อที่มีชื่อว่า “ปัญหา waiting worker”ต้นเหตุของความสับสนที่พบบ่อยที่สุดใน PWA ระดับ production:
- ผู้ใช้เปิดแอปของคุณ SW ตัวเก่า activate และควบคุมแท็บนั้น
- คุณ deploy v2 browser ติดตั้ง SW ตัวใหม่ แต่นั่งอยู่ใน Waiting
- ผู้ใช้เปิดแท็บนั้นทิ้งไว้ทั้งวัน
- SW ตัวใหม่ไม่เคย activate เลย — ผู้ใช้ไม่เคยเห็น v2
skipWaiting() — ทรงพลังแต่อันตราย
หัวข้อที่มีชื่อว่า “skipWaiting() — ทรงพลังแต่อันตราย”การเรียก self.skipWaiting() ภายใน handler ของ install ของ SW ตัวใหม่จะบังคับให้ SW ตัวนั้น activate ทันที โดยข้ามการรอ:
self.addEventListener('install', (event) => { event.waitUntil( caches.open('v2').then((cache) => cache.addAll(['/', '/app.js', '/styles.css'])) .then(() => self.skipWaiting()) );});
self.addEventListener('activate', (event) => { event.waitUntil(self.clients.claim());});อันตรายคือ: หน้าเก่ายังคงทำงานอยู่ ถ้า v2 มี cache schema หรือ asset URL ที่ต่างออกไป หน้าที่โหลดไปแล้วอาจ request ทรัพยากรที่ SW ตัวใหม่เสิร์ฟได้ไม่ถูกต้อง ผลลัพธ์คือประสบการณ์ที่พังกลางเซสชัน
คำแนะนำ: ใช้ skipWaiting() เฉพาะเมื่อ asset URL ของคุณมีการใส่เวอร์ชัน (เช่น app.abc123.js) เพื่อให้ asset ตัวเก่าและตัวใหม่เข้ากันได้ หรือไม่ก็แจ้งให้ผู้ใช้ reload เสมอ
การแจ้งให้ผู้ใช้ reload
หัวข้อที่มีชื่อว่า “การแจ้งให้ผู้ใช้ reload”แพตเทิร์นที่ปลอดภัยที่สุด: ตรวจจับ waiting worker จากหน้าเว็บแล้วแสดง banner แบบไม่รบกวน
// main.js — page codeasync function registerSw() { if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.register('/sw.js');
registration.addEventListener('updatefound', () => { const newWorker = registration.installing; if (!newWorker) return;
newWorker.addEventListener('statechange', () => { if ( newWorker.state === 'installed' && navigator.serviceWorker.controller ) { // A new SW is waiting — show update prompt showUpdateBanner(registration); } }); });}
function showUpdateBanner(registration) { const banner = document.createElement('div'); banner.textContent = 'A new version is available. '; const btn = document.createElement('button'); btn.textContent = 'Reload'; btn.onclick = () => { if (registration.waiting) { // Tell the waiting SW to skip waiting, then reload registration.waiting.postMessage({ type: 'SKIP_WAITING' }); } window.location.reload(); }; banner.appendChild(btn); document.body.prepend(banner);}
registerSw();SW ที่เกี่ยวข้องจะคอยฟังข้อความนั้น:
// sw.js — service workerself.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); }});วิธีนี้ทำให้ skipWaiting() เป็น user-driven อย่างสมบูรณ์ — เวอร์ชันใหม่จะ activate ก็ต่อเมื่อผู้ใช้คลิก Reload อย่างชัดเจนเท่านั้น เป็นการป้องกันการพังกลางเซสชัน
registration.update() — การตรวจสอบเชิงรุก
หัวข้อที่มีชื่อว่า “registration.update() — การตรวจสอบเชิงรุก”โดยค่าเริ่มต้น browser จะตรวจหาการอัปเดต SW ตอน navigation สำหรับ single-page app ที่ผู้ใช้อาจไม่เคยทริกเกอร์ navigation เลย ให้เรียก registration.update() เป็นช่วง ๆ:
setInterval(() => registration.update(), 60 * 60 * 1000); // check every hourเช็กลิสต์การอัปเดต
หัวข้อที่มีชื่อว่า “เช็กลิสต์การอัปเดต”| ขั้นตอน | ทำไม |
|---|---|
เปลี่ยนอย่างน้อยหนึ่งไบต์ใน sw.js | ทริกเกอร์การตรวจ byte-diff ของ browser |
ใส่เวอร์ชันให้ชื่อ cache (v2, v3) | ช่วยให้ activate handler ลบ cache ที่ล้าสมัยได้ |
ลบ cache เก่าใน activate | หลีกเลี่ยงการเก็บ asset ที่ล้าสมัยไว้ไม่รู้จบ |
แจ้งให้ผู้ใช้ reload แทนการบังคับ skipWaiting() | ป้องกันความไม่ตรงกันของ asset กลางเซสชัน |
เรียก clients.claim() หลัง skipWaiting() | เข้าควบคุมหน้าที่เปิดอยู่ได้ทันที |
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| แจ้งเตือนผู้ใช้ให้กด refresh เพื่ออัปเดต (prompt-based) | ผู้ใช้ควบคุม timing เอง ไม่โดนตัดงานที่ทำค้างอยู่ | ผู้ใช้ที่ไม่กด refresh อาจติดอยู่กับ version เก่าไปนาน |
| Auto-update แบบเงียบ (skipWaiting ทันที) | ผู้ใช้ได้ fix/feature ใหม่เร็วที่สุดโดยไม่ต้องทำอะไร | เสี่ยง state เพี้ยนถ้า reload กลางคันขณะผู้ใช้กำลังกรอกฟอร์มอยู่ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ใช้ cache-first HTML ที่ trap ผู้ใช้ไว้กับ shell เวอร์ชันเก่าตลอดไปเพราะไม่เคยไปแตะ network เพื่อเช็ค update เลย
- ไม่ตั้ง versioned cache name ทำให้ cache เก่ากับใหม่ปนกันตอน service worker คนละเวอร์ชันทำงานพร้อมกันชั่วคราว
- ไม่ handle statechange event เพื่อบอกผู้ใช้ว่ามี update พร้อมใช้ ทำให้ผู้ใช้ไม่รู้ตัวว่าควร refresh
💡 ตัวอย่างจากของจริง
Google Docs — ใช้ update flow ที่ตรวจ service worker ใหม่เบื้องหลังแล้วแจ้งผู้ใช้ผ่าน UI แทนการ force reload กลางที่กำลังพิมพ์งาน
Gmail — ตรวจ update เป็นระยะด้วย
registration.update()และให้ผู้ใช้เลือกเวลาที่จะรับเวอร์ชันใหม่เอง