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

The storage Event and Limits

เมื่อหน้าเว็บหนึ่งเปลี่ยนแปลง localStorage browser จะยิง storage event ไปยัง ทุกแท็บและทุกหน้าต่างอื่นที่เปิดอยู่บน origin เดียวกัน — แต่ ไม่ ยิงในแท็บที่เปลี่ยนแปลงนั้น ความไม่สมมาตรนี้ทำให้นักพัฒนาส่วนใหญ่ประหลาดใจในครั้งแรก

// Register this listener in Tab B
window.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 NOT

property ของ StorageEvent มีดังนี้:

PropertyTypeคำอธิบาย
keystring | nullkey ที่เปลี่ยนแปลง เป็น null เมื่อมีการเรียก clear()
oldValuestring | nullค่าก่อนหน้า หรือ null หาก key เพิ่งถูกสร้างใหม่
newValuestring | nullค่าใหม่ หรือ null หาก key ถูกลบ
urlstringURL ของเอกสารที่เปลี่ยนแปลง
storageAreaStorage | nullobject localStorage (ไม่เคยเป็น sessionStorage — event นั้นยิงเฉพาะภายในแท็บเดียวกัน)

storage event ยิง เฉพาะสำหรับ localStorage เท่านั้น การเปลี่ยนแปลง sessionStorage จะไม่ส่งต่อข้ามแท็บ เพราะแต่ละแท็บมี session store ของตัวเองที่แยกขาดกัน

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
storage event ยิงในแท็บอื่น ไม่ใช่ในแท็บที่เขียน

use case ที่พบบ่อยคือการ broadcast การ logout ข้ามแท็บ:

// In any tab: signal logout
localStorage.setItem('demo:auth-event', 'logout:' + Date.now());
// In every other tab: react to it
window.addEventListener('storage', (e) => {
if (e.key === 'demo:auth-event' && e.newValue?.startsWith('logout:')) {
// redirect to login page
window.location.href = '/login';
}
});

ข้อกำหนด (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 อย่างนุ่มนวล

ทุก 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 listener แล้วเขียนลง localStorage เนื่องจาก listener ยิงเฉพาะใน แท็บอื่น เท่านั้น คุณจะเห็นการยืนยันการเขียนแต่ไม่เห็น event ในหน้าต่างนี้ ลองเปิดหน้าเดียวกันในแท็บที่สองเพื่อสังเกต event ข้ามแท็บ

Browser Storage
ตัวเลือกBenefitCost
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 ทันที
  • คาดหวังว่า storage event จะยิงในแท็บที่เขียนเอง — event นี้ยิงเฉพาะในแท็บอื่นเท่านั้น ถ้าต้องการอัปเดต UI ในแท็บที่เขียนด้วย ต้องเรียก logic นั้นตรง ๆ แยกจาก listener
  • เขียนลง localStorage ทุกครั้งที่มี keystroke — เพราะทุก operation เป็น synchronous ทำให้เกิดการบล็อก main thread ถี่ ๆ จนรู้สึก jank ควร debounce ก่อนเขียนเสมอ

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

แอปแชทหรือระบบ auth หลายแท็บ — ใช้ storage event เพื่อ broadcast การ logout ข้ามแท็บ เมื่อผู้ใช้ logout จากแท็บหนึ่ง ทุกแท็บอื่นจะ redirect ไปหน้า login ทันทีโดยไม่ต้อง poll หรือใช้ WebSocket

แอปจดโน้ต/editor ที่ทำ autosave — ตรวจจับ QuotaExceededError แล้วเตือนผู้ใช้ให้ล้างฉบับร่างเก่า หรือ fallback ไปเก็บที่ IndexedDB แทนเมื่อ quota ของ localStorage เต็ม

คุณเรียก localStorage.setItem("x", "1") ใน Tab A แท็บไหนบ้างที่ได้รับ storage event?
exception อะไรที่ถูกโยนเมื่อการเขียน localStorage เกิน quota?
storage event ยิงสำหรับการเปลี่ยนแปลง sessionStorage ในอีกแท็บหนึ่งหรือไม่?
ทำไมคุณจึงควรเลี่ยงการเก็บ blob ขนาดใหญ่ใน localStorage?