-
Notifications
You must be signed in to change notification settings - Fork 0
Closed
Labels
Description
📌 Parent Epic
#144 (PWA Phase 3 - Remaining Features)
🎯 Goal
Optimize caching for specific features beyond generic strategies to maximize offline functionality and performance. Implement route-specific caching, TTL-based expiration, and intelligent prefetching.
📋 Implementation Tasks
1. Define Caching Strategy per Feature
Tasks:
- Audit all routes and categorize by caching needs
- Define strategy per route type (API, static, dynamic)
- Set TTL (Time-To-Live) per resource type
- Document caching decisions
Strategy Matrix:
| Feature | Strategy | TTL | Rationale |
|---|---|---|---|
| Secrets List | NetworkFirst | 5 min | Fresh data preferred, fallback to cache |
| Secret Details | StaleWhileRevalidate | 1 hour | Instant load, background refresh |
| Static Assets (JS/CSS) | CacheFirst | 1 year | Immutable, versioned URLs |
| Images/Avatars | CacheFirst | 30 days | Rarely change, immutable |
| API Auth | NetworkOnly | N/A | Never cache credentials |
| App Shell | CacheFirst | Forever | Core UI, versioned |
2. Implement Route-Specific Caching (Service Worker)
Tasks:
- Configure Workbox routing for each strategy
- Add cache name prefixes (api-, static-, images-)
- Set max entries per cache
- Set max age per cache
- Add cache busting for versioned assets
Implementation:
// vite.config.ts - Workbox configuration
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
workbox: {
runtimeCaching: [
// API: Secrets (NetworkFirst + TTL)
{
urlPattern: /^https:\/\/api\.secpal\.app\/v1\/secrets/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-secrets',
expiration: {
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
},
cacheableResponse: {
statuses: [0, 200],
},
networkTimeoutSeconds: 5,
},
},
// API: User Data (StaleWhileRevalidate)
{
urlPattern: /^https:\/\/api\.secpal\.app\/v1\/users/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'api-users',
expiration: {
maxEntries: 20,
maxAgeSeconds: 60 * 60, // 1 hour
},
},
},
// Images (CacheFirst + long TTL)
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
},
},
// Static Assets (CacheFirst + versioned)
{
urlPattern: /\.(?:js|css|woff2)$/,
handler: 'CacheFirst',
options: {
cacheName: 'static-assets',
expiration: {
maxEntries: 50,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
// API: Auth (NetworkOnly)
{
urlPattern: /^https:\/\/api\.secpal\.app\/v1\/auth/,
handler: 'NetworkOnly',
},
],
},
}),
],
});3. Cache Expiration Policies
Tasks:
- Implement TTL-based expiration
- Add max entries limit per cache
- Implement LRU (Least Recently Used) eviction
- Add manual cache invalidation API
- Clear expired caches on app update
Manual Invalidation:
// hooks/useCache.ts
export const useCache = () => {
const invalidateCache = async (cacheNames: string[]) => {
if (!('caches' in window)) return;
for (const name of cacheNames) {
await caches.delete(name);
}
};
const clearAllCaches = async () => {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
};
return { invalidateCache, clearAllCaches };
};4. Partial Cache Invalidation on Mutations
Tasks:
- Invalidate secrets list cache after secret creation
- Invalidate secret details cache after update
- Invalidate user cache after profile update
- Add cache invalidation to mutation hooks
- Implement optimistic cache updates
Example:
// hooks/useSecretMutation.ts
export const useCreateSecret = () => {
const { invalidateCache } = useCache();
return useMutation({
mutationFn: createSecret,
onSuccess: async () => {
// Invalidate secrets list cache
await invalidateCache(['api-secrets']);
// Trigger background sync to refresh
if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-secrets');
}
},
});
};5. Prefetching for Likely User Actions
Tasks:
- Prefetch on app idle (requestIdleCallback)
- Prefetch on hover (anticipate clicks)
- Prefetch critical resources on initial load
- Prefetch user's most-used secrets
- Add prefetch priority system
Implementation:
// hooks/usePrefetch.ts
export const usePrefetch = () => {
const prefetchOnIdle = (url: string) => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
fetch(url, { priority: 'low' });
});
}
};
const prefetchOnHover = (url: string) => {
// Prefetch when user hovers over link
return {
onMouseEnter: () => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
},
};
};
return { prefetchOnIdle, prefetchOnHover };
};
// Usage
const SecretCard = ({ secret }) => {
const { prefetchOnHover } = usePrefetch();
return (
<Link
to={`/secrets/${secret.id}`}
{...prefetchOnHover(`/api/v1/secrets/${secret.id}`)}
>
{secret.title}
</Link>
);
};6. Cache Performance Monitoring
Tasks:
- Track cache hit/miss ratio
- Measure cache lookup time
- Monitor cache size (storage quota)
- Log cache evictions
- Add performance dashboard (dev tools)
Monitoring:
// utils/cacheMonitor.ts
export class CacheMonitor {
private hits = 0;
private misses = 0;
recordHit() {
this.hits++;
this.reportMetrics();
}
recordMiss() {
this.misses++;
this.reportMetrics();
}
getHitRatio(): number {
const total = this.hits + this.misses;
return total > 0 ? this.hits / total : 0;
}
private reportMetrics() {
// Report to analytics
analytics.track({
type: 'performance',
data: {
metric: 'cache_hit_ratio',
value: this.getHitRatio() * 100,
unit: 'percent',
},
});
}
}7. WebP Optimization for Images
Tasks:
- Convert images to WebP format (build time)
- Serve WebP with fallback to PNG/JPEG
- Lazy load images (Intersection Observer)
- Add blur placeholder while loading
- Optimize image sizes (responsive images)
Lazy Loading:
// components/LazyImage.tsx
export const LazyImage: React.FC<Props> = ({ src, alt, placeholder }) => {
const [loaded, setLoaded] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
if (!imgRef.current) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = src;
img.onload = () => setLoaded(true);
observer.unobserve(img);
}
});
});
observer.observe(imgRef.current);
return () => observer.disconnect();
}, [src]);
return (
<img
ref={imgRef}
src={placeholder}
alt={alt}
className={loaded ? 'loaded' : 'loading'}
/>
);
};8. Cache Testing
Feature Tests:
- NetworkFirst returns cached data when offline
- StaleWhileRevalidate serves stale, then updates
- CacheFirst serves from cache immediately
- Cache invalidation works on mutations
- Prefetching queues requests correctly
- Cache size limits enforced
Unit Tests:
- Cache strategy selection logic
- TTL expiration calculation
- Cache key generation
- Prefetch prioritization
Coverage Target: ≥80% for new code
✅ Acceptance Criteria
- Route-specific caching strategies implemented
- TTL-based cache expiration working
- Cache invalidation on mutations functional
- Prefetching for likely actions implemented
- Cache performance monitoring in place
- WebP optimization for images
- Lazy loading for images
- Cache hit ratio >80% for returning users
- All tests passing (≥80% coverage)
- TypeScript strict mode clean
- ESLint passing
- REUSE 3.3 compliant (SPDX headers)
- CHANGELOG.md updated (Added section)
- App remains fully functional offline
🔗 Dependencies
- Depends on: Phase 1 (Service Worker, Workbox) ✅
- Part of: Epic [EPIC] Complete remaining PWA Phase 3 features (Push Notifications, Analytics, Advanced Caching) #144 (PWA Phase 3 Remaining)
- Enhances: IndexedDB (feat: Phase 3.1 - IndexedDB Setup + App Shortcuts (Quick Wins) #88), Background Sync (feat: Background Sync API with automatic retry logic (Phase 3) #90)
📝 Technical Notes
Cache Storage API Quotas
- Chrome/Edge: Dynamic (up to 60% of disk)
- Firefox: 2GB default (can request more)
- Safari: 1GB limit
Best Practices
⚠️ Use versioned cache names (e.g.,static-v1,static-v2)⚠️ Clean up old caches on service worker activation⚠️ Test on slow 3G networks (DevTools throttling)⚠️ Monitor cache sizes (prevent quota exceeded errors)
Performance Targets
- Cache Hit Ratio: >80% for returning users
- Cache Lookup Time: <50ms (p95)
- Offline Load Time: <2s (cached app shell)
- Image Load Time: <500ms (cached images)
🧪 Testing Strategy
// Example: NetworkFirst strategy
test('serves cached data when offline (NetworkFirst)', async () => {
// Populate cache
await fetch('/api/v1/secrets');
// Go offline
Object.defineProperty(navigator, 'onLine', { value: false });
// Fetch again
const response = await fetch('/api/v1/secrets');
// Should return cached data
expect(response.ok).toBe(true);
expect(response.headers.get('X-Cache')).toBe('HIT');
});
test('invalidates cache after secret creation', async () => {
// Create secret
await createSecret({ title: 'Test' });
// Check cache was invalidated
const cache = await caches.open('api-secrets');
const cachedResponse = await cache.match('/api/v1/secrets');
expect(cachedResponse).toBeUndefined();
});📝 PR Linking Instructions
When creating the PR for this sub-issue, use:
Fixes #[this-issue-number]
Closes #144- This IS the last sub-issue - use
Closes #144to close the epic - Ensure all previous sub-issues ([EPIC] Complete remaining PWA Phase 3 features (Push Notifications, Analytics, Advanced Caching) #144.1, [EPIC] Complete remaining PWA Phase 3 features (Push Notifications, Analytics, Advanced Caching) #144.2) are merged first
- Request final review before merging
Type: Sub-Issue (Frontend)
Priority: Medium
Estimated Effort: 1 week
Part of: Epic #144 (PWA Phase 3) - FINAL SUB-ISSUE
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
✅ Done