P
imvdmolen.nl
Blog

Service Workers configureren voor offline functionaliteit in progressive web apps

Een bestaande webapplicatie ombouwen naar een progressive web app die volledig offline werkt is een interessante uitdaging. Het grootste deel van de app draait al prima in de browser, maar zodra gebruikers hun verbinding verliezen verschijnen er foutmeldingen en witte schermen. Service Workers zijn de sleutel, maar het configureren ervan vraagt meer finesse dan je op het eerste gezicht verwacht.

De basis van Service Worker registratie

Timing speelt een cruciale rol bij het registreren van Service Workers. Te vroeg registreren kan de initiële laadtijd van je applicatie beïnvloeden, terwijl te laat registreren betekent dat gebruikers langer moeten wachten voordat offline functionaliteit beschikbaar is. Het sweet spot ligt bij het load event, wanneer de pagina volledig geladen is maar voordat gebruikers daadwerkelijk problemen ondervinden met hun verbinding.

if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(registration => {
                console.log('SW geregistreerd: ', registration);
                
                registration.addEventListener('updatefound', () => {
                    const newWorker = registration.installing;
                    newWorker.addEventListener('statechange', () => {
                        if (newWorker.state === 'installed') {
                            if (navigator.serviceWorker.controller) {
                                // Nieuwe versie beschikbaar
                                showUpdateNotification();
                            }
                        }
                    });
                });
            })
            .catch(registrationError => {
                console.log('SW registratie gefaald: ', registrationError);
            });
    });
}

Browser ondersteuning controleer ik altijd vooraf. Hoewel Service Workers tegenwoordig breed ondersteund worden, voorkom ik graag dat mijn code errors gooit in oudere browsers. De feature detection met 'serviceWorker' in navigator is een simpele maar effectieve manier om dit te voorkomen.

Update handling vind ik een van de meest onderbelichte aspecten van Service Worker implementatie. Gebruikers moeten weten wanneer er een nieuwe versie van de app beschikbaar is, en ik geef ze altijd de keuze om direct bij te werken of pas bij de volgende sessie. Deze transparantie voorkomt verwarring wanneer features plotseling anders werken dan verwacht.

Caching strategieën implementeren

Verschillende typen content vereisen verschillende caching strategieën. Voor statische assets zoals CSS, JavaScript en afbeeldingen implementeer ik een "Cache First" strategie. Deze assets veranderen zelden en wanneer ze dat wel doen, gebeurt dat meestal bij een volledige applicatie-update.

const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
    '/',
    '/styles/main.css',
    '/scripts/app.js',
    '/images/logo.png',
    '/manifest.json'
];

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                return cache.addAll(urlsToCache);
            })
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                // Cache hit - return response
                if (response) {
                    return response;
                }

                return fetch(event.request).then(response => {
                    // Controleer of we een geldige response hebben
                    if (!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }

                    // Clone de response omdat het een stream is
                    const responseToCache = response.clone();

                    caches.open(CACHE_NAME)
                        .then(cache => {
                            cache.put(event.request, responseToCache);
                        });

                    return response;
                });
            })
    );
});

API calls behandel ik met een "Network First" benadering. Gebruikers krijgen dan altijd de meest recente data wanneer ze online zijn, maar kunnen nog steeds functioneren met gecachede data wanneer de verbinding wegvalt. Deze strategie vereist wel meer complexe error handling en fallback logica, maar het resultaat is een veel natuurlijkere gebruikerservaring.

Response cloning is een technisch detail waar ik aanvankelijk tegenaan liep. Een response stream kan maar één keer gelezen worden, dus als je zowel de response wilt cachen als naar de client wilt sturen, moet je de response clonen. Dit is een van die Service Worker quirks die je leert door ervaring.

Selectief cachen van dynamische content doe ik met veel voorzichtigheid. Niet alle API responses zijn geschikt voor langdurige caching, vooral niet die met gebruikersspecifieke data. Productcatalogi, nieuwsartikelen en configuratie-instellingen cache ik wel, omdat het niet erg is als deze iets verouderd zijn tijdens offline gebruik.

Geavanceerde offline scenario's

Formuliergebaseerde applicaties stellen bijzondere eisen aan offline functionaliteit. Een van mijn meest uitdagende projecten was een app waar gebruikers data moesten kunnen invoeren, zelfs zonder internetverbinding. Dit betekende het bouwen van een mechanisme voor het opslaan van form submissions en deze later synchroniseren.

// In de Service Worker
self.addEventListener('sync', event => {
    if (event.tag === 'background-sync') {
        event.waitUntil(doBackgroundSync());
    }
});

async function doBackgroundSync() {
    const db = await openDB();
    const pendingRequests = await db.getAll('pending-requests');
    
    for (const request of pendingRequests) {
        try {
            await fetch(request.url, {
                method: request.method,
                body: request.body,
                headers: request.headers
            });
            
            // Verwijder uit pending requests
            await db.delete('pending-requests', request.id);
            
            // Notify de applicatie over succesvolle sync
            self.registration.showNotification('Data gesynchroniseerd', {
                body: 'Je offline wijzigingen zijn opgeslagen.',
                icon: '/icons/icon-192x192.png'
            });
        } catch (error) {
            console.log('Sync failed voor request:', request.id);
        }
    }
}

IndexedDB werd mijn beste vriend voor complexe offline data opslag. Het biedt veel meer flexibiliteit dan localStorage en kan grote hoeveelheden gestructureerde data aan. Ik gebruik het voor het cachen van API responses, het opslaan van pending form submissions, en zelfs voor het bijhouden van gebruikersvoorkeuren die offline beschikbaar moeten zijn.

Background Sync zorgt voor automatische synchronisatie zodra de verbinding terugkeert. Dit gebeurt zelfs wanneer de gebruiker de applicatie heeft gesloten, wat de user experience aanzienlijk verbetert. Gebruikers hoeven niet handmatig te controleren of hun wijzigingen zijn doorgevoerd, wat veel stress wegneemt uit de offline ervaring.

Conflict resolution bij data synchronisatie vraagt om doordachte strategieën. Wat gebeurt er als een gebruiker offline data heeft gewijzigd die online ook door iemand anders is aangepast? Soms kies ik voor "last write wins", soms voor het bewaren van beide versies en gebruikers laten kiezen. Het hangt af van de applicatie en de impact van verkeerde keuzes.

Debugging en monitoring van Service Workers

Service Workers debuggen heeft zijn eigen uitdagingen. Chrome DevTools biedt uitstekende tools, maar ik heb geleerd dat uitgebreide logging onmisbaar is. Service Workers draaien in een andere context dan je hoofdapplicatie, waardoor traditionele debugging technieken niet altijd werken.

// Uitgebreide logging voor Service Worker events
self.addEventListener('install', event => {
    console.log('[SW] Install event', event);
    console.log('[SW] Caching app shell');
    
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => {
            console.log('[SW] Cache opened');
            return cache.addAll(urlsToCache);
        }).then(() => {
            console.log('[SW] All files cached');
        }).catch(error => {
            console.error('[SW] Failed to cache files:', error);
        })
    );
});

self.addEventListener('fetch', event => {
    // Log alleen niet-Chrome extension requests
    if (event.request.url.startsWith('http')) {
        console.log('[SW] Fetch event for:', event.request.url);
    }
    
    event.respondWith(
        caches.match(event.request).then(response => {
            if (response) {
                console.log('[SW] Cache hit for:', event.request.url);
                return response;
            }
            
            console.log('[SW] Network request for:', event.request.url);
            return fetch(event.request);
        }).catch(error => {
            console.error('[SW] Fetch failed:', error);
            // Fallback page voor navigatie requests
            if (event.request.mode === 'navigate') {
                return caches.match('/offline.html');
            }
        })
    );
});

Chrome extension requests kunnen je logging vervuilen als je niet oppast. Browser extensies maken constant requests die door je Service Worker worden opgevangen, maar die hebben niets te maken met je applicatie. Filteren op URL schema helpt om relevante logs te scheiden van ruis.

Cache management vergt voortdurende aandacht. Service Worker caches kunnen snel groeien en gebruikers onnodige ruimte kosten. Daarom implementeer ik altijd mechanismen voor het opschonen van verouderde caches en het beperken van de cache grootte. Een cache die te groot wordt, kan ironisch genoeg de performance verslechteren in plaats van verbeteren.

Versioning strategieën zijn cruciaal bij Service Worker updates. Wanneer ik een nieuwe versie deploy, moet de oude Service Worker netjes worden afgesloten en vervangen. Dit betekent meestal het updaten van cache namen en het verwijderen van oude caches tijdens de activate fase. Zonder goede versioning kunnen gebruikers vastzitten met verouderde content.

Performance metrics voor offline functionaliteit verzamel ik door custom events naar analytics te sturen wanneer gebruikers offline gaan of terug online komen. Deze data geeft me inzicht in hoe vaak offline functionaliteit daadwerkelijk wordt gebruikt en waar verbeteringen het meest impact hebben.

Robuuste Service Workers hebben mijn perspectief op web development veranderd. Het is niet meer voldoende om alleen te denken aan de happy path waarin alles werkt, maar ook aan alle scenario's waarin dingen mis kunnen gaan. Die mindset resulteert uiteindelijk in betere applicaties, zelfs buiten PWA context. Offline-first development dwingt je om na te denken over edge cases, error handling en gebruikerservaring op een manier die alle aspecten van je code verbetert.