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

การอัปเดต Service Worker

การ deploy PWA เวอร์ชันใหม่ก็คือการ deploy service worker ตัวใหม่ การเข้าใจว่า browser ตรวจจับและนำการอัปเดตนั้นมาใช้อย่างไร — และจะสื่อสารกับผู้ใช้อย่างไร — ช่วยป้องกันบั๊กที่แอบแฝงและความประหลาดใจเรื่อง asset ที่ล้าสมัย

browser จะตรวจไฟล์ service worker ของคุณว่ามีการเปลี่ยนแปลงหรือไม่ในทุกครั้งที่โหลดหน้า (และทุกครั้งที่คุณเรียก registration.update() เอง) โดยเปรียบเทียบไฟล์ที่ดาวน์โหลดมาใหม่กับ SW ที่ติดตั้งอยู่ปัจจุบันแบบ byte-for-byte แม้ความต่างเพียง ไบต์เดียว — string เวอร์ชันของ cache ที่เปลี่ยนไป, comment ใหม่, การปรับ whitespace — ก็ทริกเกอร์ flow การอัปเดต:

  1. SW ตัวใหม่ถูกดาวน์โหลดและ parse
  2. SW ตัวใหม่เข้าสู่เฟส install
  3. ถ้าการติดตั้งสำเร็จ SW ตัวใหม่จะนั่งอยู่ใน Waiting — ไม่สามารถ activate ได้ขณะที่ SW ตัวเก่ายังควบคุมแท็บที่เปิดอยู่

ต้นเหตุของความสับสนที่พบบ่อยที่สุดใน PWA ระดับ production:

  • ผู้ใช้เปิดแอปของคุณ SW ตัวเก่า activate และควบคุมแท็บนั้น
  • คุณ deploy v2 browser ติดตั้ง SW ตัวใหม่ แต่นั่งอยู่ใน Waiting
  • ผู้ใช้เปิดแท็บนั้นทิ้งไว้ทั้งวัน
  • SW ตัวใหม่ไม่เคย activate เลย — ผู้ใช้ไม่เคยเห็น v2

การเรียก 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 เสมอ

แพตเทิร์นที่ปลอดภัยที่สุด: ตรวจจับ waiting worker จากหน้าเว็บแล้วแสดง banner แบบไม่รบกวน

// main.js — page code
async 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 worker
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

วิธีนี้ทำให้ skipWaiting() เป็น user-driven อย่างสมบูรณ์ — เวอร์ชันใหม่จะ activate ก็ต่อเมื่อผู้ใช้คลิก Reload อย่างชัดเจนเท่านั้น เป็นการป้องกันการพังกลางเซสชัน

โดยค่าเริ่มต้น 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()เข้าควบคุมหน้าที่เปิดอยู่ได้ทันที
ตัวเลือกBenefitCost
แจ้งเตือนผู้ใช้ให้กด 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() และให้ผู้ใช้เลือกเวลาที่จะรับเวอร์ชันใหม่เอง

browser ตัดสินว่าไฟล์ service worker เปลี่ยนไปได้อย่างไร?
ทำไม service worker ที่เพิ่งติดตั้งจึงนั่งอยู่ใน Waiting?
ความเสี่ยงของการเรียก self.skipWaiting() แบบไม่มีเงื่อนไขใน install handler คืออะไร?
แนวทางใดที่ทำให้ skipWaiting() เป็น user-driven และหลีกเลี่ยงการพังกลางเซสชัน?