🎯 Mục lục
🔍 Cache API là gì?
Cache API là một Web API chuẩn được thiết kế để lưu trữ HTTP requests/responses một cách persistent (bền vững). Nó là một phần của Service Worker specification nhưng có thể sử dụng độc lập trong main thread.
✨ Đặc điểm chính:
- Persistent Storage: Data được lưu trên disk, tồn tại qua các session
- Origin-scoped: Mỗi origin có cache riêng biệt
- Request/Response based: Làm việc với HTTP Request/Response objects
- Asynchronous: All operations trả về Promises
- Browser-managed: Browser quản lý lifecycle và cleanup
🏗️ Cấu trúc:
// Global caches object
caches: CacheStorage └── open(cacheName) → Cache ├── match(request) → Response | undefined ├── put(request, response) → void ├── add(request) → void ├── delete(request) → boolean └── keys() → Request[]
⚙️ Cách hoạt động của Cache API
1. Cache Storage Management
// Mở hoặc tạo cache
const cache = await caches.open('my-cache-v1'); // List tất cả cache names
const cacheNames = await caches.keys(); // Xóa cache
const deleted = await caches.delete('old-cache');
2. Storing Responses
// Cách 1: Fetch và cache manual
const response = await fetch(url);
await cache.put(url, response.clone()); // Cách 2: Cache tự động fetch
await cache.add(url); // Cách 3: Batch operations
await cache.addAll([url1, url2, url3]);
3. Retrieving Cached Data
// Tìm exact match
const cachedResponse = await cache.match(request); // Tìm trong tất cả caches
const response = await caches.match(request); // Với options
const response = await cache.match(request, { ignoreSearch: true, // Bỏ qua query params ignoreMethod: false, // Chỉ match GET requests ignoreVary: false // Respect Vary header
});
4. Cache Lifecycle
class CacheManager { constructor() { this.CACHE_NAME = 'app-cache-v1'; this.CACHE_VERSION = 1; } async updateCache() { // Xóa old versions const cacheNames = await caches.keys(); await Promise.all( cacheNames .filter(name => name !== this.CACHE_NAME) .map(name => caches.delete(name)) ); }
}
🚀 Implementation trong dự án
Dựa trên code của bạn, đây là cách Cache API được sử dụng:
1. Selective Caching Strategy
// Từ selective-cache-demo.js
class SelectiveImageDemo { constructor() { this.imageCacheName = 'selective-image-cache-v1'; // Bộ lọc thông minh this.requestClassifier = { isImage(url, contentType = null) { // Logic phân loại hình ảnh const imagePatterns = [/picsum\.photos/, /via\.placeholder/]; const imageMimeTypes = ['image/jpeg', 'image/png']; return imagePatterns.some(p => p.test(url)) || imageMimeTypes.some(m => contentType?.includes(m)); }, isAPI(url) { return /\/api\/|jsonplaceholder|api\./.test(url); }, isAsset(url) { return /\.(css|js|woff)(\?|$)/.test(url); } }; } async loadAndClassifyResource(url, cache) { // 1. Phân loại request trước khi fetch const classification = this.classifyRequest(url); // 2. Fetch từ network const response = await fetch(url); // 3. Quyết định cache dựa trên classification if (classification.shouldCache) { await cache.put(url, response.clone()); // ✅ Cache this.log(`✅ CACHED: ${url}`, 'success'); } else { this.log(`⏭️ SKIPPED: ${url} - ${classification.reason}`, 'warning'); } return response; }
}
2. Smart Classification Logic
// Phân loại thông minh dựa trên:
classifyRequest(url, contentType = null) { // 🖼️ Images → CACHE if (this.requestClassifier.isImage(url, contentType)) { return { type: 'image', shouldCache: true, reason: 'Image detected - safe to cache' }; } // 🔗 API → SKIP (data có thể thay đổi) if (this.requestClassifier.isAPI(url)) { return { type: 'api', shouldCache: false, reason: 'API call - data might change' }; } // 📄 Assets → SKIP (cần strategy riêng) if (this.requestClassifier.isAsset(url)) { return { type: 'asset', shouldCache: false, reason: 'Asset - separate cache strategy needed' }; } return { type: 'unknown', shouldCache: false };
}
📊 So sánh: Cache API vs Map-based Interceptor
🏆 Cache API Approach
✅ Ưu điểm:
-
Persistent Storage
// Data tồn tại qua browser restarts const cache = await caches.open('my-cache'); await cache.put(url, response); // → Vẫn có sau khi restart browser
-
Automatic Serialization
// Cache tự động serialize Response objects const response = await fetch('/api/data'); await cache.put(url, response); // Auto serialize const cached = await cache.match(url); // Auto deserialize
-
Standards-based
- Web standard, được hỗ trợ rộng rãi
- Tương thích với Service Workers
- Security model built-in
-
Storage Management
// Browser tự động quản lý storage limits if ('storage' in navigator && 'estimate' in navigator.storage) { const estimate = await navigator.storage.estimate(); console.log(`Used: ${estimate.usage}, Available: ${estimate.quota}`); }
-
Request Matching
// Flexible matching options await cache.match(request, { ignoreSearch: true, // /api/data?v=1 matches /api/data?v=2 ignoreMethod: false, // Chỉ match GET requests ignoreVary: false // Respect Vary headers });
❌ Nhược điểm:
-
Complexity
// Phức tạp hơn Map const cache = await caches.open('my-cache'); await cache.put(url, response.clone());
-
Performance Overhead
- Disk I/O operations
- Serialization/deserialization cost
-
Async-only
// Không có sync operations const cached = await cache.match(url); // Must await
🗺️ Map-based Interceptor Approach
✅ Ưu điểm:
-
Simplicity & Speed
class MapCache { constructor() { this.cache = new Map(); } set(url, data) { this.cache.set(url, data); // Instant } get(url) { return this.cache.get(url); // Instant } }
-
Synchronous Access
// Không cần await const cached = cache.get(url); if (cached) return cached;
-
Flexible Data Types
// Có thể cache bất kỳ data type nào cache.set('user-123', { id: 123, name: 'John' }); cache.set('config', new Configuration()); cache.set('computed-result', computeExpensiveValue());
-
Custom Logic
class SmartMapCache { set(key, value, ttl = 3600000) { // 1 hour default this.cache.set(key, { data: value, timestamp: Date.now(), ttl: ttl }); } get(key) { const entry = this.cache.get(key); if (!entry) return null; // TTL check if (Date.now() - entry.timestamp > entry.ttl) { this.cache.delete(key); return null; } return entry.data; } }
-
Integration with Interceptors
// Axios interceptor example axios.interceptors.request.use(config => { const cached = mapCache.get(config.url); if (cached && shouldUseCache(config)) { // Return cached response return Promise.resolve({ data: cached, fromCache: true }); } return config; }); axios.interceptors.response.use(response => { if (shouldCache(response.config)) { mapCache.set(response.config.url, response.data); } return response; });
❌ Nhược điểm:
-
Memory-only
// Mất data khi refresh page const cache = new Map(); // → Lost on page reload
-
Manual Management
// Phải tự quản lý memory leaks class ManagedMapCache { constructor(maxSize = 100) { this.cache = new Map(); this.maxSize = maxSize; } set(key, value) { // Manual cleanup khi quá size if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, value); } }
-
No Persistence
- Không tồn tại qua sessions
- Không offline support
🎯 Khi nào nên sử dụng phương pháp nào?
🌐 Sử dụng Cache API khi:
-
PWA/Offline Support
// Cần offline functionality self.addEventListener('fetch', event => { if (event.request.destination === 'image') { event.respondWith( caches.match(event.request) .then(cached => cached || fetch(event.request)) ); } });
-
Large Assets/Resources
// Cache images, videos, large files const mediaCache = await caches.open('media-cache-v1'); await mediaCache.add('/videos/intro.mp4'); // 50MB video
-
Cross-session Persistence
// Data cần tồn tại lâu dài const userDataCache = await caches.open('user-data'); await userDataCache.put('/api/user/profile', response); // → Vẫn có sau 1 tuần
-
Standards Compliance
// Dự án enterprise, cần tuân thủ standards class StandardsCompliantCache { async cacheResource(url) { const cache = await caches.open(this.cacheName); return cache.add(url); } }
🗺️ Sử dụng Map-based khi:
-
High-frequency Access
// Cache computed values, hot data class ComputeCache { compute(input) { const cached = this.cache.get(input); if (cached) return cached; // Instant const result = expensiveComputation(input); this.cache.set(input, result); return result; } }
-
Session-based Cache
// Data chỉ cần trong session hiện tại class SessionCache { constructor() { this.userPreferences = new Map(); this.temporaryData = new Map(); } }
-
Complex Cache Logic
class IntelligentCache { set(key, value, metadata = {}) { this.cache.set(key, { data: value, priority: metadata.priority || 1, accessed: Date.now(), hits: 0 }); } get(key) { const entry = this.cache.get(key); if (entry) { entry.hits++; entry.accessed = Date.now(); return entry.data; } return null; } // LRU eviction strategy evictLRU() { let oldestKey = null; let oldestTime = Date.now(); for (const [key, entry] of this.cache) { if (entry.accessed < oldestTime) { oldestTime = entry.accessed; oldestKey = key; } } if (oldestKey) this.cache.delete(oldestKey); } }
-
Development/Testing
// Dễ debug và test class DebuggableCache { set(key, value) { console.log(`Cache SET: ${key}`); this.cache.set(key, value); } get(key) { const hit = this.cache.has(key); console.log(`Cache ${hit ? 'HIT' : 'MISS'}: ${key}`); return hit ? this.cache.get(key) : null; } }
🎨 Hybrid Approach: Kết hợp cả hai
class HybridCache { constructor() { this.memoryCache = new Map(); // Fast access this.persistentCacheName = 'hybrid-cache-v1'; } async get(key) { // 1. Check memory first (fastest) if (this.memoryCache.has(key)) { return this.memoryCache.get(key); } // 2. Check persistent cache const cache = await caches.open(this.persistentCacheName); const cached = await cache.match(key); if (cached) { const data = await cached.json(); // Promote to memory cache this.memoryCache.set(key, data); return data; } return null; } async set(key, value, options = {}) { // Always store in memory this.memoryCache.set(key, value); // Store in persistent cache if needed if (options.persist) { const cache = await caches.open(this.persistentCacheName); const response = new Response(JSON.stringify(value)); await cache.put(key, response); } }
} // Usage
const hybridCache = new HybridCache(); // Fast, memory-only
await hybridCache.set('temp-data', data); // Persistent across sessions
await hybridCache.set('user-settings', settings, { persist: true });
🛠️ Best Practices
1. Cache Naming & Versioning
class VersionedCache { constructor() { this.version = '1.2.0'; this.cacheName = `app-cache-v${this.version}`; } async migrate() { const cacheNames = await caches.keys(); const oldCaches = cacheNames.filter(name => name.startsWith('app-cache-') && name !== this.cacheName ); // Cleanup old versions await Promise.all(oldCaches.map(name => caches.delete(name))); }
}
2. Selective Caching Strategy
class SelectiveCacheStrategy { shouldCache(request, response) { // Không cache error responses if (!response.ok) return false; // Cache images lâu dài if (request.destination === 'image') { return { cache: true, ttl: '30d' }; } // Cache API responses ngắn hạn if (request.url.includes('/api/')) { return { cache: true, ttl: '5m' }; } // Không cache dynamic content if (request.url.includes('?nocache=')) { return { cache: false }; } return { cache: true, ttl: '1h' }; }
}
3. Error Handling
class RobustCache { async safeGet(key) { try { const cache = await caches.open(this.cacheName); const cached = await cache.match(key); return cached ? await cached.json() : null; } catch (error) { console.warn('Cache read error:', error); return null; // Graceful degradation } } async safeSet(key, data) { try { const cache = await caches.open(this.cacheName); const response = new Response(JSON.stringify(data)); await cache.put(key, response); return true; } catch (error) { console.warn('Cache write error:', error); return false; // Continue without caching } }
}
4. Storage Monitoring
class StorageMonitor { async checkQuota() { if ('storage' in navigator && 'estimate' in navigator.storage) { const estimate = await navigator.storage.estimate(); const usagePercent = (estimate.usage / estimate.quota) * 100; if (usagePercent > 80) { console.warn('Storage quota nearly full:', usagePercent + '%'); await this.cleanup(); } } } async cleanup() { const caches = await caches.keys(); // Remove oldest caches first const oldestCache = caches.sort().shift(); if (oldestCache) { await caches.delete(oldestCache); } }
}
📝 Kết luận
Cache API phù hợp cho:
- ✅ PWA và offline support
- ✅ Caching assets lớn (images, videos)
- ✅ Long-term persistence
- ✅ Standards compliance
Map-based Cache phù hợp cho:
- ✅ High-performance, in-memory caching
- ✅ Complex cache logic
- ✅ Session-based data
- ✅ Development và testing
Hybrid Approach tốt nhất cho:
- 🏆 Production apps cần cả performance và persistence
- 🏆 Multi-layer caching strategy
- 🏆 Flexible cache policies
Việc lựa chọn phụ thuộc vào use case cụ thể của dự án. Cache API mạnh về persistence và standards, trong khi Map-based cache ưu việt về performance và flexibility. Kết hợp cả hai sẽ cho hiệu quả tối ưu nhất!