Progressive Web Apps (PWA)
Offline Support
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