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

0 0 0

Người đăng: Vũ Trần Lê Anh

Theo Viblo Asia

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

  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!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Flutter - GetX - Using GetConnect to handle API request (Part 4)

Giới thiệu. Xin chào các bạn, lại là mình với series về GetX và Flutter.

0 0 385

- vừa được xem lúc

API vs WebSockets vs WebHooks: What to Choose?

. Khi xây dựng bất kì một ứng dụng nào, chúng ta đều cần phải có một cơ chế đáng tin cậy để giao tiếp giữa các thành phần của nó. Đây là khi APIs, WebSockets và WebHooks được ứng dụng vào.

0 0 118

- vừa được xem lúc

Sử dụng Fast JSON API serialization trong Ruby on Rails

Ở bài viết này chúng ta sẽ thử tạo 1 project API sử dụng gem fast_jsonapi cho serializer. Đầu tiên là tạo một project API mới. $ rails new rails-jsonapi --database=postgresql --skip-action-mailbox --skip-action-text --skip-spring -T --skip-turbolinks --api. .

0 0 162

- vừa được xem lúc

Test thử ba loại API chụp màn hình Windows

Hiện tại, Windows cung cấp khoảng ba cách để chụp màn hình. Thế thì cái nào là nhanh nhất? Tôi muốn test thử từng cái.

0 0 98

- vừa được xem lúc

Ngừng sử dụng REST cho API — GraphQL là cách tốt hơn

Mở đầu. REST đã được nhiều developers sử dụng để gửi dữ liệu qua HTTP trong khi GraphQL thường được trình bày như một công nghệ thay thế các API REST.

0 0 128

- vừa được xem lúc

Quản lý và sử dụng API trong Nuxt bằng cách sử dụng Repository Pattern

Mở đầu năm mới, à nhầm, mở đầu bài viết. Cái tên NuxtJS chắc hẳn cũng không còn xa lạ gì với những bạn yêu thích VueJS nữa, đương nhiên mình cũng là một chàng trai dành tình yêu to lớn cho frameworks này.

0 0 256