Offline Support in PWAs
One of the most powerful features of Progressive Web Apps is the ability to work offline. By implementing proper offline support, your app can provide a seamless experience even when the user loses network connectivity.
Detecting Offline Status
JavaScript provides the navigator.onLine property to check network connectivity status:
<script>
// Check current online status
if (navigator.onLine) {
console.log('Connected to the internet');
} else {
console.log('No internet connection');
}
// Listen for online/offline events
window.addEventListener('online', () => {
console.log('Connection restored');
updateUIForOnlineMode();
});
window.addEventListener('offline', () => {
console.log('Connection lost');
updateUIForOfflineMode();
});
function updateUIForOnlineMode() {
document.body.classList.remove('offline-mode');
document.body.classList.add('online-mode');
showNotification('You are back online!', 'success');
}
function updateUIForOfflineMode() {
document.body.classList.remove('online-mode');
document.body.classList.add('offline-mode');
showNotification('You are offline. Some features may be limited.', 'warning');
}
</script>
Warning: The navigator.onLine property is not 100% reliable. It only detects if there's a network connection, not if the internet is actually accessible. A device might be connected to a WiFi network that has no internet access, and navigator.onLine would still return true.
Creating an Offline Page
A common pattern is to cache a dedicated offline page that is shown when the user tries to navigate while offline:
// In your service worker (sw.js)
const OFFLINE_PAGE = '/offline.html';
const CACHE_NAME = 'offline-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
OFFLINE_PAGE,
'/styles/offline.css',
'/images/offline-icon.svg'
]);
})
);
self.skipWaiting();
});
self.addEventListener('fetch', (event) => {
// Only handle navigate requests (HTML pages)
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(OFFLINE_PAGE);
})
);
}
});
Offline Page HTML Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>You're Offline</title>
<link rel="stylesheet" href="/styles/offline.css">
</head>
<body>
<div class="offline-container">
<img src="/images/offline-icon.svg" alt="Offline">
<h1>You're Offline</h1>
<p>It looks like you've lost your internet connection.</p>
<p>Don't worry, you can still access previously viewed content.</p>
<button onclick="window.history.back()">Go Back</button>
<button onclick="location.reload()">Try Again</button>
</div>
</body>
</html>
Graceful Degradation Strategies
When implementing offline support, consider these UX patterns:
Best Practices for Offline UX:
- Visual Indicators: Show a banner or icon when offline
- Disable Features: Gray out or hide features that require connectivity
- Queue Actions: Save user actions locally and sync when online
- Cached Content: Show previously loaded content with timestamps
- Helpful Messages: Explain what works and what doesn't offline
Offline Data Storage
Store data locally for offline access using various storage mechanisms:
// Using localStorage for simple data
function saveOfflineData(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
console.log('Data saved for offline use');
} catch (e) {
console.error('Failed to save offline data:', e);
}
}
function getOfflineData(key) {
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (e) {
console.error('Failed to retrieve offline data:', e);
return null;
}
}
// Check online status before fetching
async function fetchWithOfflineFallback(url, storageKey) {
if (navigator.onLine) {
try {
const response = await fetch(url);
const data = await response.json();
saveOfflineData(storageKey, data);
return data;
} catch (error) {
console.warn('Fetch failed, using cached data');
return getOfflineData(storageKey);
}
} else {
return getOfflineData(storageKey);
}
}
Advanced Offline Detection
For more reliable offline detection, implement a heartbeat check:
class ConnectionMonitor {
constructor(checkInterval = 30000) {
this.checkInterval = checkInterval;
this.isOnline = navigator.onLine;
this.listeners = [];
this.init();
}
init() {
// Listen to browser events
window.addEventListener('online', () => this.updateStatus(true));
window.addEventListener('offline', () => this.updateStatus(false));
// Periodic heartbeat check
setInterval(() => this.checkConnection(), this.checkInterval);
}
async checkConnection() {
try {
const response = await fetch('/ping', {
method: 'HEAD',
cache: 'no-cache'
});
this.updateStatus(response.ok);
} catch (error) {
this.updateStatus(false);
}
}
updateStatus(isOnline) {
if (this.isOnline !== isOnline) {
this.isOnline = isOnline;
this.listeners.forEach(callback => callback(isOnline));
}
}
onChange(callback) {
this.listeners.push(callback);
}
}
// Usage
const monitor = new ConnectionMonitor();
monitor.onChange((isOnline) => {
console.log(isOnline ? 'Online' : 'Offline');
updateUI(isOnline);
});
Note: The heartbeat check adds network overhead. Use a reasonable interval (30-60 seconds) to balance responsiveness with resource usage.
Offline-First Architecture
In an offline-first approach, the app always tries to load from cache first, then updates from the network:
// Service Worker: Cache-first strategy
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
// Return cached version if available
if (cachedResponse) {
// Update cache in background
fetch(event.request).then((networkResponse) => {
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse);
});
});
return cachedResponse;
}
// Otherwise fetch from network
return fetch(event.request).then((networkResponse) => {
// Cache the new response
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
});
Exercise:
- Create an offline.html page with custom styling
- Update your service worker to cache and serve the offline page
- Add visual indicators in your app to show online/offline status
- Implement localStorage fallback for API data
- Test by disabling your network connection