0

📚 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
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí