+1

📚 Cache API: Lý Thuyết và So Sánh với Map-based Cache

🎯 Mục lục

  1. Cache API là gì?
  2. Cách hoạt động của Cache API
  3. Implementation trong dự án
  4. So sánh: Cache API vs Map-based Interceptor
  5. Khi nào nên sử dụng phương pháp nào?
  6. Best Practices

🔍 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:

  1. 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
    
  2. 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
    
  3. Standards-based

    • Web standard, được hỗ trợ rộng rãi
    • Tương thích với Service Workers
    • Security model built-in
  4. 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}`);
    }
    
  5. 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:

  1. Complexity

    // Phức tạp hơn Map
    const cache = await caches.open('my-cache');
    await cache.put(url, response.clone());
    
  2. Performance Overhead

    • Disk I/O operations
    • Serialization/deserialization cost
  3. Async-only

    // Không có sync operations
    const cached = await cache.match(url); // Must await
    

🗺️ Map-based Interceptor Approach

Ưu điểm:

  1. 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
        }
    }
    
  2. Synchronous Access

    // Không cần await
    const cached = cache.get(url);
    if (cached) return cached;
    
  3. 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());
    
  4. 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;
        }
    }
    
  5. 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:

  1. Memory-only

    // Mất data khi refresh page
    const cache = new Map();
    // → Lost on page reload
    
  2. 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);
        }
    }
    
  3. 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:

  1. 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))
            );
        }
    });
    
  2. Large Assets/Resources

    // Cache images, videos, large files
    const mediaCache = await caches.open('media-cache-v1');
    await mediaCache.add('/videos/intro.mp4'); // 50MB video
    
  3. 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
    
  4. 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:

  1. 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;
        }
    }
    
  2. Session-based Cache

    // Data chỉ cần trong session hiện tại
    class SessionCache {
        constructor() {
            this.userPreferences = new Map();
            this.temporaryData = new Map();
        }
    }
    
  3. 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);
        }
    }
    
  4. 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!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.