App Shell Architecture
App-shell architecture
หัวข้อที่มีชื่อว่า “App-shell architecture”App-shell architecture คือ design pattern ที่แบ่ง PWA ออกเป็นสองชั้นที่แตกต่างกัน:
- Shell — HTML, CSS, และ JavaScript ขั้นต่ำที่ render โครงของแอป (navigation, layout, header, footer) ส่วนนี้เปลี่ยนแปลงเฉพาะเมื่อคุณ deploy version ใหม่
- Content — ข้อมูลและ markup เฉพาะสำหรับแต่ละ view (บทความ, สินค้า, โปรไฟล์ผู้ใช้) ส่วนนี้เปลี่ยนแปลงบ่อยและถูก fetch ที่ runtime
โดยการแยกสองชั้นนี้ คุณสามารถ precache shell อย่างจริงจังและใช้กลยุทธ์ที่เบากว่าสำหรับ content ผลลัพธ์คือการเข้าชมซ้ำโหลด UI frame จาก local storage ในมิลลิวินาที และ content จะ fill in แบบ asynchronous
อะไรที่ควรอยู่ใน shell
หัวข้อที่มีชื่อว่า “อะไรที่ควรอยู่ใน shell”Shell ควรมีทุกอย่างที่จำเป็นในการ render skeleton ที่มีความหมายโดยไม่มี network request:
index.html — application entry pointapp.css — layout, typography, navigation stylesapp.js — client-side router และ bootstrap codeicons/logo.svg — brand assets ที่ใช้ในทุกหน้าoffline.html — fallback page สำหรับ navigation ที่ล้มเหลวไม่ควรมี per-page data, user-specific content, หรือ assets ที่เปลี่ยนแปลงระหว่าง page views
Precaching shell ใน install
หัวข้อที่มีชื่อว่า “Precaching shell ใน install”const SHELL_CACHE = 'shell-v2';const SHELL_ASSETS = [ '/', '/app.css', '/app.js', '/icons/logo.svg', '/offline.html',];
self.addEventListener('install', (event) => { event.waitUntil( caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS)) ); self.skipWaiting();});cache.addAll() เป็น atomic — หาก asset ใดหนึ่งดาวน์โหลดล้มเหลว การ install ทั้งหมดจะล้มเหลวและ SW เก่ายังคงทำงาน ควรให้ precache list มีขนาดเล็กและเชื่อถือได้
Cache shell, network content
หัวข้อที่มีชื่อว่า “Cache shell, network content”ใน fetch handler ให้แยกแยะ shell requests จาก content requests และใช้กลยุทธ์ที่แตกต่างกันกับแต่ละอัน
const CONTENT_CACHE = 'content-v1';
self.addEventListener('fetch', (event) => { const url = new URL(event.request.url);
// Shell assets: cache-first (เร็ว, offline-safe) if (SHELL_ASSETS.includes(url.pathname)) { event.respondWith( caches.match(event.request).then((cached) => cached || fetch(event.request)) ); return; }
// API / content: network-first, fall back to cache if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(event.request) .then((response) => { const clone = response.clone(); caches.open(CONTENT_CACHE).then((cache) => cache.put(event.request, clone)); return response; }) .catch(() => caches.match(event.request)) ); return; }
// Navigation: serve shell, ให้ SPA router จัดการ route if (event.request.mode === 'navigate') { event.respondWith( caches.match('/').then((cached) => cached || fetch(event.request)) ); }});ทำไม SPA pattern ถึงสำคัญที่นี่
หัวข้อที่มีชื่อว่า “ทำไม SPA pattern ถึงสำคัญที่นี่”ใน Single Page Application server มี HTML file เพียงหนึ่งไฟล์ (index.html / /) การ routing ทั้งหมดเกิดขึ้นใน JavaScript เมื่อ service worker ดักจับ navigation ไปยัง /profile หรือ /settings ควรให้บริการ index.html shell เดิม — SPA router จะอ่าน URL และ render view ที่ถูกต้อง
Load sequence diagram
หัวข้อที่มีชื่อว่า “Load sequence diagram”sequenceDiagram
participant U as User
participant SW as Service Worker
participant CS as Cache Storage
participant NET as Network
U->>SW: navigate to /dashboard
SW->>CS: match('/')
CS-->>SW: shell (index.html) — instant
SW-->>U: Shell renders in <100ms
U->>SW: fetch /api/dashboard-data
SW->>NET: fetch (network-first)
NET-->>SW: JSON response
SW->>CS: cache.put (content-v1)
SW-->>U: Data fills in การอัปเดต shell
หัวข้อที่มีชื่อว่า “การอัปเดต shell”เพราะ shell assets ถูก cache อย่างจริงจัง คุณต้องมีกลยุทธ์ในการ push การอัปเดต แนวทางมาตรฐานคือการเปลี่ยน cache name (เช่น shell-v2 → shell-v3) และ clean up old caches ใน activate event
self.addEventListener('activate', (event) => { const keepCaches = [SHELL_CACHE, CONTENT_CACHE]; event.waitUntil( caches.keys().then((names) => Promise.all( names .filter((name) => !keepCaches.includes(name)) .map((name) => caches.delete(name)) ) ).then(() => self.clients.claim()) );});ลองใช้งานจริง
หัวข้อที่มีชื่อว่า “ลองใช้งานจริง”ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| App shell architecture | โหลด UI shell ได้ทันทีจาก cache แม้ offline แล้วค่อยเติมข้อมูล | ต้องแยก shell กับ content ตั้งแต่ตอนออกแบบ เพิ่ม complexity ของ build |
| Server-render เต็มหน้าทุกครั้ง | โครงสร้างง่ายกว่า ไม่ต้องแยก shell/content | ไม่มีอะไรให้แสดงเลยถ้า network ล้มเหลว เพราะทั้งหน้าขึ้นกับ response เดียว |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- cache เฉพาะ shell แต่ลืมว่า content ที่ dynamic ต้องมี fallback ของตัวเองด้วย ไม่งั้น offline จะเห็น shell เปล่าไม่มีข้อมูล
- ทำ shell ใหญ่เกินไปจนแทบเท่าทั้งหน้า ทำให้เสียประโยชน์ของการแยก shell ออกจาก content
- ไม่ versioned shell cache ทำให้ deploy ใหม่แล้ว shell เก่ายังค้างอยู่ ผู้ใช้เห็น UI เก่าปนกับ data ใหม่
💡 ตัวอย่างจากของจริง
Google Developers site — เป็นตัวอย่างต้นแบบของ app shell model ที่ Google ใช้สอนแนวคิดนี้ตั้งแต่แรกเริ่ม
Twitter Lite — แยก shell (navigation, layout) ออกจาก timeline content ทำให้เปิดแอปได้ทันทีแม้เน็ตช้าก่อนค่อยโหลด tweet