Background Sync in PWAs
Background Sync allows your PWA to defer actions until the user has stable connectivity. This ensures that user actions are preserved and completed even if the network is unreliable at the moment of the action.
What is Background Sync?
Background Sync is a web API that enables service workers to defer tasks until the user has a stable internet connection. It's perfect for ensuring that user actions (like sending messages, submitting forms, or uploading files) are completed successfully.
Key Benefits:
- Reliability: User actions complete even after temporary network failures
- Better UX: Users don't have to retry failed actions manually
- Offline Support: Actions queued offline are synced when back online
- Battery Efficient: Browser manages the sync timing optimally
Registering a Sync Event
Request a sync event to be triggered when the browser detects stable connectivity:
// In your main application code
async function registerSync(tag) {
try {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register(tag);
console.log('Sync registered:', tag);
} catch (error) {
console.error('Sync registration failed:', error);
}
}
// Example: Queue a message to be sent
async function sendMessage(message) {
// Save message to IndexedDB
await saveToIndexedDB('pending-messages', message);
// Register sync event
if ('sync' in registration) {
await registerSync('sync-messages');
console.log('Message queued for sync');
} else {
// Fallback: Try to send immediately
await sendMessageToServer(message);
}
}
// Usage
document.getElementById('send-btn').addEventListener('click', async () => {
const message = document.getElementById('message-input').value;
await sendMessage({
text: message,
timestamp: Date.now()
});
});
Handling Sync Events in Service Worker
Listen for sync events in your service worker and perform the deferred actions:
// In sw.js
self.addEventListener('sync', (event) => {
console.log('Sync event triggered:', event.tag);
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
} else if (event.tag === 'sync-posts') {
event.waitUntil(syncPosts());
}
});
async function syncMessages() {
try {
// Retrieve pending messages from IndexedDB
const messages = await getFromIndexedDB('pending-messages');
if (messages.length === 0) {
console.log('No messages to sync');
return;
}
// Send each message to the server
for (const message of messages) {
try {
const response = await fetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message)
});
if (response.ok) {
// Remove from IndexedDB after successful send
await removeFromIndexedDB('pending-messages', message.id);
console.log('Message synced:', message.id);
} else {
throw new Error('Server error');
}
} catch (error) {
console.error('Failed to sync message:', error);
// Keep in IndexedDB for next sync attempt
}
}
console.log('Messages sync complete');
} catch (error) {
console.error('Sync failed:', error);
throw error; // Retry sync later
}
}
Sync Best Practices:
- Use descriptive tag names (e.g., 'sync-messages', 'sync-photos')
- Store pending actions in IndexedDB for persistence
- Remove items from queue only after successful sync
- Handle errors gracefully to allow retry attempts
- Implement reasonable timeout limits
Retry Logic
If a sync attempt fails, the browser will automatically retry based on various factors:
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-data') {
event.waitUntil(
syncData()
.catch((error) => {
console.error('Sync failed, will retry:', error);
// Browser will automatically retry
// Throw error to signal failure
throw error;
})
);
}
});
async function syncData() {
const maxRetries = 3;
let attempts = 0;
while (attempts < maxRetries) {
try {
const data = await getFromIndexedDB('pending-data');
const response = await fetch('/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
await clearIndexedDB('pending-data');
return; // Success
}
attempts++;
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
} catch (error) {
attempts++;
if (attempts >= maxRetries) {
throw error;
}
}
}
}
Browser Limitations: The browser controls when sync events fire and may delay them to optimize battery and network usage. Don't rely on sync events firing immediately or at specific times.
Periodic Background Sync
Periodic Background Sync allows your PWA to synchronize data at regular intervals, even when the app is not open:
// Register periodic sync (requires user permission)
async function registerPeriodicSync() {
try {
const registration = await navigator.serviceWorker.ready;
// Check if periodic sync is supported
if ('periodicSync' in registration) {
const status = await navigator.permissions.query({
name: 'periodic-background-sync'
});
if (status.state === 'granted') {
await registration.periodicSync.register('update-content', {
minInterval: 24 * 60 * 60 * 1000 // 24 hours
});
console.log('Periodic sync registered');
} else {
console.log('Periodic sync permission denied');
}
}
} catch (error) {
console.error('Periodic sync registration failed:', error);
}
}
// In sw.js
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'update-content') {
event.waitUntil(updateContent());
}
});
async function updateContent() {
try {
const response = await fetch('/api/latest-content');
const content = await response.json();
// Cache the content
const cache = await caches.open('content-cache');
await cache.put('/api/latest-content', new Response(JSON.stringify(content)));
console.log('Content updated via periodic sync');
} catch (error) {
console.error('Content update failed:', error);
}
}
Common Use Cases
Perfect for Background Sync:
- Messaging: Queue messages to be sent when online
- Form Submissions: Save form data and submit when connected
- File Uploads: Upload photos/documents when network is stable
- Analytics: Send analytics data in batches
- Comments/Reviews: Post user feedback when back online
Complete Example: Offline Form Submission
// In main app
async function submitForm(formData) {
try {
// Save to IndexedDB
await saveToIndexedDB('pending-forms', {
id: Date.now(),
data: formData,
timestamp: Date.now()
});
// Register sync
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-forms');
// Show success message
showNotification('Form saved. Will submit when online.');
} catch (error) {
console.error('Failed to queue form:', error);
showNotification('Failed to save form. Please try again.');
}
}
// In sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-forms') {
event.waitUntil(syncForms());
}
});
async function syncForms() {
const forms = await getFromIndexedDB('pending-forms');
for (const form of forms) {
try {
const response = await fetch('/api/submit-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.data)
});
if (response.ok) {
await removeFromIndexedDB('pending-forms', form.id);
// Show notification
self.registration.showNotification('Form Submitted', {
body: 'Your form has been submitted successfully',
icon: '/images/success-icon.png'
});
}
} catch (error) {
console.error('Form sync failed:', error);
}
}
}
Exercise:
- Create a simple message app that queues messages offline
- Implement Background Sync to send messages when back online
- Store pending messages in IndexedDB
- Add retry logic for failed sync attempts
- Show notifications when messages are successfully synced
- Test by going offline, sending messages, then reconnecting