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

Offline Fallback Page

การปรับปรุงที่ง่ายที่สุดที่คุณสามารถทำกับ PWA ที่ยังไม่มีกลยุทธ์ offline คือการเพิ่มหน้า offline fallback หน้าเดียว เมื่อผู้ใช้นำทางไปยังไซต์และเครือข่ายไม่พร้อมใช้งาน แทนที่จะแสดงหน้า error ของ browser พวกเขาจะเห็นหน้าที่มี branding ของคุณที่บอกว่าพวกเขา offline

  1. ใน install — precache offline.html เพื่อให้พร้อมใช้ทันที แม้ก่อนที่ผู้ใช้จะเคยเยี่ยมชมหน้าอื่น
  2. ใน fetch — ดักจับ navigation requests (event.request.mode === 'navigate'), ลองเครือข่าย, และ catch ความล้มเหลวโดยให้บริการ offline.html ที่ cache ไว้

รูปแบบนี้จงใจให้เรียบง่าย ไม่ดักจับ sub-resources (รูปภาพ, script, API calls) — เฉพาะ top-level page navigations เท่านั้น ทำให้ logic เรียบง่ายและหลีกเลี่ยงผลข้างเคียงที่ไม่ตั้งใจต่อ API calls

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

event.waitUntil() ทำให้ install phase ทำงานต่อจนกว่า Promise จะ resolve เพื่อให้แน่ใจว่าไฟล์ถูก cache ก่อนที่ SW จะ activate self.skipWaiting() ทำให้ SW ใหม่เข้าควบคุมทันทีโดยไม่ต้องรออันเก่า

self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
});

event.request.mode === 'navigate' เป็น true เฉพาะสำหรับ navigation requests ระดับบนสุดเท่านั้น — การคลิกลิงก์, การป้อน URL, หรือการรีเฟรชหน้า Sub-resource requests (CSS, JS, XHR) มี modes อื่น ('cors', 'no-cors', 'same-origin') ดังนั้น guard นี้จะดักจับเฉพาะ page loads เท่านั้น

self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});

self.clients.claim() ทำให้ service worker เข้าควบคุมหน้าที่เปิดอยู่แล้วโดยไม่ต้องรีโหลด หากไม่มี หน้าปัจจุบันจะไม่ถูกควบคุมจนกว่าจะนำทางครั้งต่อไป

sequenceDiagram
  participant U as User
  participant SW as Service Worker
  participant N as Network
  participant C as Cache Storage

  U->>SW: navigate to /page (mode: navigate)
  SW->>N: fetch(request)
  alt Network available
    N-->>SW: 200 OK Response
    SW-->>U: Page renders normally
  else Network unavailable
    N--xSW: fetch() rejects
    SW->>C: caches.match('/offline.html')
    C-->>SW: cached offline.html
    SW-->>U: Offline fallback page
  end
ขั้นตอน navigation fetch พร้อม offline fallback

Demo ด้านล่างลงทะเบียน service worker ที่ precaches offline.html เปิด StackBlitz preview จากนั้นใช้แท็บ Network ของ DevTools เพื่อเปลี่ยนเป็น offline mode แล้วรีโหลดหน้า — คุณควรเห็น offline fallback แทน dinosaur

Runs a real service worker + manifest in your browser.

หน้า offline fallback ที่ดีควร:

  • ตรงกับ brand ของคุณ — ฟอนต์, สี, และโลโก้เดียวกับแอปที่เหลือ
  • อธิบายสถานการณ์อย่างชัดเจน — “You are offline” ดีกว่า error ทั่วไป
  • มีปุ่มลองใหม่location.reload() เพียงพอแล้ว
  • เป็น self-contained อย่างสมบูรณ์ — ไม่มี external stylesheets หรือ scripts เพราะสิ่งเหล่านั้นก็จะล้มเหลวเมื่อ offline เช่นกัน
  • มีขนาดเล็ก — ต่ำกว่า 5 KB แบบบีบอัดเพื่อให้ precache ได้ทันที
ตัวเลือกBenefitCost
Serve หน้า offline fallback ที่ออกแบบเองUX ดีกว่ามาก ผู้ใช้รู้สถานะและทำอะไรต่อได้ (เช่นดูของที่ cache ไว้)ต้องออกแบบและ maintain หน้าเพิ่มอีกหนึ่งหน้า
ปล่อยให้ browser แสดง error page มาตรฐาน (dino game ของ Chrome)ไม่ต้องทำอะไรเพิ่มผู้ใช้ไม่รู้ว่าแอปมีอะไรให้ใช้ offline ได้บ้าง ประสบการณ์แย่กว่ามาก
  • ไม่ precache หน้า offline fallback เอง ทำให้ตอนที่ network ล้มเหลวจริงๆ กลับโหลดหน้า fallback ไม่ได้เช่นกัน (chicken-and-egg problem)
  • แสดง fallback แบบเดียวกันทุก route โดยไม่บอกผู้ใช้ว่าตอนนี้กำลังพยายามโหลดอะไรอยู่
  • ลืมทดสอบ fallback บนกรณีที่ network “ช้ามาก” ไม่ใช่แค่ “ขาดหายสนิท” ทำให้ UX พังตอนเน็ตแค่แย่ไม่ถึงกับหลุด

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

Chrome dino game — เป็น offline fallback แบบ built-in ของ browser เอง ที่แสดงให้เห็นว่า “ไม่มีอะไรให้ทำ” นั้น UX แย่แค่ไหนเทียบกับแอปที่เตรียม fallback เอง

The Guardian / BBC News — แสดงหน้า offline ที่บอกชัดว่าไม่มีสัญญาณ พร้อมลิงก์ไปบทความที่เคย cache ไว้ให้อ่านต่อได้ทันที

ค่า event.request.mode ใดที่บ่งบอกว่าเป็น navigation ของหน้าระดับบนสุด?
ทำไมจึงเรียก self.skipWaiting() ใน install event?
event.waitUntil() ทำอะไรใน install handler?
ทำไมหน้า fallback offline.html จึงควร self-contained อย่างสมบูรณ์ (ไม่มี CSS หรือ JS ภายนอก)?