Progressive Web Apps (PWA)

Background Sync

17 min Lesson 9 of 30

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:
  1. Create a simple message app that queues messages offline
  2. Implement Background Sync to send messages when back online
  3. Store pending messages in IndexedDB
  4. Add retry logic for failed sync attempts
  5. Show notifications when messages are successfully synced
  6. Test by going offline, sending messages, then reconnecting