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

fetch Event

เมื่อ service worker ถูก activate และควบคุมหน้าเว็บแล้ว ทุก network request ที่หน้าเว็บส่งออกไป — HTML, CSS, JS, รูปภาพ, การเรียก API — จะผ่าน fetch event ของ SW นี่คือหัวใจของ PWA แบบ offline-first

ภายใน listener ของ fetch ให้เรียก event.respondWith() ด้วย Promise ที่ resolve ออกมาเป็น Response ถ้าคุณไม่เรียก event.respondWith() request จะตกไปยัง network ตามปกติ (พฤติกรรมแบบ passthrough)

self.addEventListener('fetch', (event) => {
// Simplest possible passthrough — behaves as if no SW is installed
event.respondWith(fetch(event.request));
});

ลอง network ก่อน ถ้าล้มเหลว (ออฟไลน์หรือ server error) ค่อย fall back ไปที่ cache

self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone the response: a Response body can only be consumed once
const clone = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
});

เสิร์ฟจาก cache ทันที ถ้ายังไม่ได้ cache ไว้ค่อยไปที่ network แล้ว cache ผลลัพธ์ไว้

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, clone));
return response;
});
})
);
});

คืนเวอร์ชันที่ cache ไว้ในทันที แล้วค่อยอัปเดต cache อยู่เบื้องหลัง

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) =>
cache.match(event.request).then((cached) => {
const networkFetch = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || networkFetch;
})
)
);
});
sequenceDiagram
  participant P as Page
  participant SW as Service Worker
  participant C as Cache Storage
  participant N as Network

  P->>SW: fetch(request)
  SW->>C: caches.match(request)
  alt cache hit
    C-->>SW: cached Response
    SW-->>P: Response (instant)
  else cache miss
    SW->>N: fetch(request)
    N-->>SW: fresh Response
    SW->>C: cache.put(request, response.clone())
    SW-->>P: Response
  end
การไหลของ fetch แบบ cache-first ผ่าน service worker

clone ก่อน cache เสมอ body ของ Response คือ readable stream ที่ consume ได้เพียงครั้งเดียว ถ้าคุณส่ง Response ตัวเดียวกันให้ทั้ง caller และ cache.put() ฝั่งใดฝั่งหนึ่งจะได้ body ว่างเปล่า ใช้ response.clone() ก่อนจัดเก็บ

อย่าดักจับ cross-origin opaque response อย่างประมาท request ไปยัง third-party origin ที่ไม่มี CORS จะคืน opaque response (type === 'opaque') ที่มี status === 0 การ cache opaque response ทำให้พื้นที่จัดเก็บ cache บวมขึ้นอย่างคาดเดาไม่ได้ ใช้ตรรกะของ fetch-event เฉพาะกับ request แบบ same-origin เท่านั้น เว้นแต่คุณจะรู้ว่ากำลังทำอะไรอยู่

self.addEventListener('fetch', (event) => {
// Only handle same-origin requests
if (!event.request.url.startsWith(self.location.origin)) return;
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});
ตัวเลือกBenefitCost
Intercept ทุก fetch event และ respondWith() เองควบคุม caching strategy ได้ละเอียดทุก requestทุก request ต้อง handle ให้ถูก ไม่งั้น fetch อาจ hang หรือ error
ปล่อยให้ request ผ่านไปหา network ตามปกติ (ไม่ intercept)โค้ดง่าย ไม่เสี่ยง bug จาก caching logicไม่ได้ประโยชน์ offline หรือความเร็วจาก cache เลย
  • ไม่เรียก event.respondWith() เลยในบาง path ทำให้ browser fallback ไป network แบบไม่ตั้งใจ (เงียบๆ ไม่มี error)
  • cache response ของ POST/PUT request ที่ไม่ควร cache ทำให้ mutation ทำงานผิดพลาดตอน replay จาก cache
  • ไม่เช็ค request.mode หรือ request.method ก่อน intercept ทำให้ fetch handler ไปยุ่งกับ request ที่ควรปล่อยผ่าน เช่น cross-origin analytics call

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

Gmail — fetch handler ของ service worker แยก strategy ระหว่าง static asset กับ API call ของอีเมลอย่างชัดเจน ทำให้ offline ยังเปิดอีเมลที่เคยอ่านได้

Google Docs — intercept fetch event เพื่อ serve เอกสารจาก cache ทันทีเมื่อออฟไลน์ แล้ว sync การแก้ไขกลับตอนกลับมา online

จะเกิดอะไรขึ้นถ้า fetch handler เรียก event.respondWith() ด้วย Promise ที่ reject?
ทำไมจึงต้อง clone Response ก่อนส่งให้ cache.put()?
กลยุทธ์ fetch แบบใดที่คืน cached response ทันทีและอัปเดต cache อยู่เบื้องหลัง?
opaque response คืออะไร?