Imagine a shopping app that suggests the perfect product even when the user is riding a subway with no signal.
Why offline AI matters for Progressive Web Apps
Users expect instant, context‑aware suggestions regardless of connectivity. According to a 2023 mobile usage report, 43% of sessions start offline, yet only 12% of apps deliver meaningful functionality without a network. Combining a PWA’s installable, native‑like feel with on‑device inference bridges that gap, turning latency into a competitive advantage.
TensorFlow.js brings the power of neural networks to the browser, while Service Workers and IndexedDB guarantee that the model and user data persist across launches. The result is a truly offline‑first experience that still feels personalized.
Setting up the Service Worker for data caching
The first step is to register a Service Worker that caches static assets and the TensorFlow model files. A minimal sw.js might look like this:
self.addEventListener('install', event => { const urls = ['/index.html','/styles.css','/app.js','/model/model.json']; event.waitUntil(caches.open('pwa-assets').then(cache => cache.addAll(urls))); }); self.addEventListener('fetch', event => { if (event.request.url.includes('/model/')) { event.respondWith(caches.match(event.request).then(resp => resp || fetch(event.request).then(fetchResp => { const clone = fetchResp.clone(); caches.open('pwa-assets').then(cache => cache.put(event.request, clone)); return fetchResp; }))); } else { event.respondWith(fetch(event.request)); } }); Register the worker from your main script:
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(reg => { console.log('SW registered', reg); }).catch(err => console.error('SW registration failed', err)); } With this setup, the model JSON and binary weight files are stored the first time the user goes online, making them instantly available on subsequent offline sessions.
Storing user interactions in IndexedDB
Personalization requires a history of clicks, searches, or purchases. IndexedDB offers a fast, transactional key‑value store that survives page reloads and even browser restarts. Using the tiny idb wrapper simplifies the API:
import { openDB } from 'https://cdn.jsdelivr.net/npm/idb@7/+esm'; const dbPromise = openDB('pwa-reco', 1, { upgrade(db) { db.createObjectStore('interactions', {keyPath: 'id', autoIncrement:true}); } }); export async function logInteraction(itemId, score) { const db = await dbPromise; await db.add('interactions', {itemId, score, ts: Date.now()}); } When the app launches, you can retrieve the last 100 entries to feed the recommendation engine:
export async function getRecentInteractions(limit=100) { const db = await dbPromise; return db.getAllFromIndex('interactions', 'ts', IDBKeyRange.lowerBound(0), 'prev', limit); } Because IndexedDB runs in the background thread, these reads do not block the UI, keeping the experience smooth.
Running TensorFlow.js models on the client
TensorFlow.js works with both pre‑trained Keras models converted to the TensorFlow.js format and models built directly in JavaScript. Loading the model is straightforward:
async function loadModel() { const model = await tf.loadLayersModel('/model/model.json'); return model; } Assume the model expects a 1‑D tensor of the last five item IDs encoded as integers. You can prepare the input from IndexedDB data and run inference without ever leaving the device:
async function recommend() { const model = await loadModel(); const recent = await getRecentInteractions(5); const ids = recent.map(r => r.itemId); const input = tf.tensor2d([ids], [1, ids.length]); const scores = model.predict(input); const topIdx = scores.argMax(-1).dataSync()[0]; return ids[topIdx]; } All calculations happen in the browser’s WebGL or WASM backend, delivering predictions in under 30 ms on a typical mid‑range phone (e.g., Snapdragon 750G).
Personalizing recommendations without a network
Combine the cached model with the interaction history to generate a ranked list. A practical pattern is to merge model scores with simple heuristics such as recency weight or inventory availability stored in a local JSON file. Example:
async function getOfflineRecommendations() { const modelScore = await recommend(); const inventory = await fetch('/data/inventory.json').then(r => r.json()); const item = inventory.find(i => i.id === modelScore); const finalScore = item.popularity * 0.6 + modelScore * 0.4; return {item, finalScore}; } This hybrid approach ensures that even if the model is imperfect, business rules keep the suggestions relevant.
Performance tuning and fallback strategies
To keep the offline bundle lightweight, prune unused layers from the TensorFlow model and compress the weight files with gzip (the Service Worker will serve the compressed version automatically). Monitor memory usage with Chrome’s Performance panel; a 2 MB model typically consumes < 150 MB RAM on Android Chrome, well within safe limits.
If the device lacks GPU acceleration, force TensorFlow.js to use the CPU backend:
tf.setBackend('cpu'); await tf.ready(); Finally, always provide a graceful degradation path: when the model fails to load, fall back to a rule‑based recommendation engine that reads the same IndexedDB data. This guarantees that users never see an empty state.
Sources
Google Developers – Progressive Web Apps documentation
TensorFlow.js – Official guide and API reference
MDN Web Docs – Service Worker and IndexedDB tutorials
Author: Mahmut Sarıkaya — sarikayadev.com