The storage Event and Limits
storage event
หัวข้อที่มีชื่อว่า “storage event”เมื่อหน้าเว็บหนึ่งเปลี่ยนแปลง localStorage browser จะยิง storage event ไปยัง ทุกแท็บและทุกหน้าต่างอื่นที่เปิดอยู่บน origin เดียวกัน — แต่ ไม่ ยิงในแท็บที่เปลี่ยนแปลงนั้น ความไม่สมมาตรนี้ทำให้นักพัฒนาส่วนใหญ่ประหลาดใจในครั้งแรก
// Register this listener in Tab Bwindow.addEventListener('storage', (event) => { console.log('key changed:', event.key); console.log('old value:', event.oldValue); console.log('new value:', event.newValue); console.log('storage area:', event.storageArea); // localStorage object});
// In Tab A, write to localStorage — Tab B's listener fires; Tab A's does NOTproperty ของ StorageEvent มีดังนี้:
| Property | Type | คำอธิบาย |
|---|---|---|
key | string | null | key ที่เปลี่ยนแปลง เป็น null เมื่อมีการเรียก clear() |
oldValue | string | null | ค่าก่อนหน้า หรือ null หาก key เพิ่งถูกสร้างใหม่ |
newValue | string | null | ค่าใหม่ หรือ null หาก key ถูกลบ |
url | string | URL ของเอกสารที่เปลี่ยนแปลง |
storageArea | Storage | null | object localStorage (ไม่เคยเป็น sessionStorage — event นั้นยิงเฉพาะภายในแท็บเดียวกัน) |
storage event ยิง เฉพาะสำหรับ localStorage เท่านั้น การเปลี่ยนแปลง sessionStorage จะไม่ส่งต่อข้ามแท็บ เพราะแต่ละแท็บมี session store ของตัวเองที่แยกขาดกัน
แพตเทิร์นการส่งสัญญาณข้ามแท็บ (Cross-tab signalling)
หัวข้อที่มีชื่อว่า “แพตเทิร์นการส่งสัญญาณข้ามแท็บ (Cross-tab signalling)”sequenceDiagram
participant A as Tab A (writer)
participant LS as localStorage
participant B as Tab B (listener)
A->>LS: setItem("demo:signal", "ping")
LS-->>B: storage event fires
Note over B: key="demo:signal"<br/>newValue="ping"
Note over A: No event fires in Tab A use case ที่พบบ่อยคือการ broadcast การ logout ข้ามแท็บ:
// In any tab: signal logoutlocalStorage.setItem('demo:auth-event', 'logout:' + Date.now());
// In every other tab: react to itwindow.addEventListener('storage', (e) => { if (e.key === 'demo:auth-event' && e.newValue?.startsWith('logout:')) { // redirect to login page window.location.href = '/login'; }});quota ราว ~5 MB
หัวข้อที่มีชื่อว่า “quota ราว ~5 MB”ข้อกำหนด (specification) ของ Web Storage ไม่ได้บังคับขนาด quota ที่ตายตัว แต่ browser ต่าง ๆ implement ที่ประมาณ 5 MB ต่อ origin สำหรับ localStorage เหมือนกันทั่วไป (บางตัวให้ 10 MB; browser บนมือถืออาจให้น้อยกว่า)
เมื่อคุณใช้เกิน quota setItem จะโยน QuotaExceededError (ที่เป็น DOMException):
try { const big = 'x'.repeat(6 * 1024 * 1024); // 6 MB string localStorage.setItem('demo:big', big);} catch (e) { if (e instanceof DOMException && e.name === 'QuotaExceededError') { console.error('Storage quota exceeded — cannot save data'); }}ให้ครอบการเขียนที่อาจมีขนาดใหญ่ (เช่น array หรือ object ที่ serialise แล้วและโตขึ้นเรื่อย ๆ ตามเวลา) ด้วย try/catch เสมอ เพื่อจัดการ quota error อย่างนุ่มนวล
ข้อควรระวังเรื่องการบล็อกแบบ synchronous
หัวข้อที่มีชื่อว่า “ข้อควรระวังเรื่องการบล็อกแบบ synchronous”ทุก operation ของ Web Storage — setItem, getItem, removeItem, clear — ทำงานแบบ synchronous บน main thread ปกติแล้วไม่มีปัญหากับ value เล็ก ๆ แต่จะกลายเป็นปัญหาเมื่อ:
- คุณกำลังเก็บ string ขนาดใหญ่ (หลักร้อย KB ขึ้นไป) — แต่ละการเรียกจะบล็อก main thread ขณะ serialise และเขียน
- คุณเรียก
setItemใน loop ที่ถี่ — การบล็อกสะสมรวมกันมากขึ้น - คุณอ่านค่าขนาดใหญ่ภายใน
requestAnimationFrameหรือ event handler — frame drop จะมองเห็นได้ชัด
หากคุณพบว่าตัวเองกำลังเก็บ blob ขนาดใหญ่ ลองพิจารณา IndexedDB (asynchronous รองรับข้อมูล binary) หรือ Cache API แทน
รันได้: storage event listener + การเขียน
หัวข้อที่มีชื่อว่า “รันได้: storage event listener + การเขียน”โค้ดด้านล่างจะลงทะเบียน storage listener แล้วเขียนลง localStorage เนื่องจาก listener ยิงเฉพาะใน แท็บอื่น เท่านั้น คุณจะเห็นการยืนยันการเขียนแต่ไม่เห็น event ในหน้าต่างนี้ ลองเปิดหน้าเดียวกันในแท็บที่สองเพื่อสังเกต event ข้ามแท็บ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
storage event | ไม่ต้องพึ่ง library ภายนอก ใช้ browser API ล้วน ๆ สำหรับส่งสัญญาณข้ามแท็บ | ทำงานเฉพาะกับ localStorage เท่านั้น และไม่ยิงในแท็บที่เขียนเอง ต้องออกแบบ logic ให้รองรับความไม่สมมาตรนี้ |
| quota ~5 MB ของ Web Storage | ขนาดเล็กพอที่ browser จะประมวลผล synchronous ได้เร็ว ไม่ต้องตั้งค่าอะไรเพิ่ม | เมื่อข้อมูลโตเกิน 5 MB ต้องย้ายไป IndexedDB ทั้งระบบ ซึ่งเปลี่ยน API และ mental model ทั้งหมด |
| API แบบ synchronous | เขียนโค้ดง่าย ไม่ต้องจัดการ Promise หรือ callback | ค่าขนาดใหญ่จะบล็อก main thread ตรง ๆ ทำให้ UI ค้างระหว่างอ่าน/เขียน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- เขียน
setItemโดยไม่ครอบtry/catch— เมื่อ quota เต็มsetItemจะโยนQuotaExceededErrorและถ้าไม่ดักไว้ แอปจะ crash ทันที - คาดหวังว่า
storageevent จะยิงในแท็บที่เขียนเอง — event นี้ยิงเฉพาะในแท็บอื่นเท่านั้น ถ้าต้องการอัปเดต UI ในแท็บที่เขียนด้วย ต้องเรียก logic นั้นตรง ๆ แยกจาก listener - เขียนลง
localStorageทุกครั้งที่มี keystroke — เพราะทุก operation เป็น synchronous ทำให้เกิดการบล็อก main thread ถี่ ๆ จนรู้สึก jank ควร debounce ก่อนเขียนเสมอ
💡 ตัวอย่างจากของจริง
แอปแชทหรือระบบ auth หลายแท็บ — ใช้
storageevent เพื่อ broadcast การ logout ข้ามแท็บ เมื่อผู้ใช้ logout จากแท็บหนึ่ง ทุกแท็บอื่นจะ redirect ไปหน้า login ทันทีโดยไม่ต้อง poll หรือใช้ WebSocketแอปจดโน้ต/editor ที่ทำ autosave — ตรวจจับ
QuotaExceededErrorแล้วเตือนผู้ใช้ให้ล้างฉบับร่างเก่า หรือ fallback ไปเก็บที่ IndexedDB แทนเมื่อ quota ของlocalStorageเต็ม