Naar de inhoud
← Blog

Techniek

Een PWA naast een native app

ReadyToBinge draait als PWA in de browser en als aparte app in de winkels. Wat een PWA technisch vraagt, wat service workers doen en waar iOS de grens trekt.

9 november 2026 · 6 min lezen

ReadyToBinge is een trailerfeed in React en TypeScript, met Cloudflare erachter en data van TMDB. In de browser is het een progressive web app, of PWA, die je als webapp op je telefoon kan zetten. Daarnaast staat er een aparte native app in de App Store en op Google Play.

Die app kwam er omdat mensen een app in de winkel verwachten, en omdat ik een app nog altijd de betere keuze vind als mensen iets vooral op hun telefoon gebruiken. Heb je echt native dingen nodig, zoals camera, locatie of meldingen, dan kies je best een echte mobiele app. Kan het met een responsive webapp, dan volstaat die. Voor de keuze zelf is er App, webapp of website. Hier gaat het over wat de PWA technisch vraagt, en waar ze stopt.

Wat een PWA is

Een PWA is een website op HTTPS met een web app manifest en meestal een service worker. Het manifest vertelt de browser hoe de app heet, welke iconen ze heeft en hoe ze opent. Met standalone valt de adresbalk weg en lijkt ze op een gewone app. Dit is een vereenvoudigde illustratie, niet het manifest van ReadyToBinge:

{
  "name": "Voorbeeldapp",
  "short_name": "Voorbeeld",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#1b1510",
  "theme_color": "#1b1510",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}

Wat een service worker doet

Een service worker is een script dat los van de pagina draait en tussen de pagina en het netwerk zit. Het kan verzoeken van de pagina’s binnen zijn scope onderscheppen en beantwoorden uit de cache. Ook web push loopt erlangs.

De strategie hangt af van het soort verzoek. Bestanden met een hash in de naam kan je gerust cache-first serveren, omdat de naam verandert als de inhoud verandert. HTML haal je best eerst van het netwerk, met de cache als terugval, want anders blijft iemand op een oude versie van de app hangen. Data van een API hangt af van hoe vers ze moet zijn. Video laat je best met rust. Die bestanden zijn groot en werken met range requests, en een feed vol trailers zou je opslagquotum snel opgebruiken.

// Vereenvoudigde illustratie, niet de code van ReadyToBinge.
self.addEventListener("fetch", (event) => {
  const { request } = event;
  const url = new URL(request.url);

  if (request.destination === "video") return; // de browser handelt dit af

  if (request.mode === "navigate") {
    event.respondWith(fetch(request).catch(() => caches.match("/offline")));
    return;
  }

  if (url.pathname.startsWith("/assets/")) {
    event.respondWith(caches.match(request).then((hit) => hit ?? fetch(request)));
  }
});

Het lastigste deel is updaten. Een nieuwe service worker blijft wachten tot alle tabs of vensters met de oude versie dicht zijn, en een geïnstalleerde app blijft vaak lang open. Je kan skipWaiting gebruiken, maar dan kan een pagina die nog draait plots een nieuwe worker krijgen en bestanden vragen die er niet meer zijn. Laat oude bestanden met een hash een tijdje staan op je server, of toon de gebruiker een melding dat er een nieuwe versie klaarstaat.

Installeren

Hoe een browser beslist of je kan installeren, verandert, dus controleer de actuele regels. Chrome verwacht een manifest met onder meer een naam en iconen. De eis van een service worker met een fetch-handler verviel voor de installatie via het menu vanaf versie 108 op mobiel en 112 op desktop.

In browsers op Chromium kan je het event beforeinstallprompt opvangen en op een zelfgekozen moment een eigen knop tonen. Dat event is geen standaard, en Safari heeft het niet.

Op iOS installeert iemand via het deelmenu en “Zet op beginscherm”. Je kan dat niet vanuit je code starten, dus je zal het zelf in je interface moeten uitleggen. Met de mediaquery display-mode: standalone kan je nagaan of de app al geïnstalleerd draait, en die uitleg dan verbergen.

Dat is een stap meer dan in een appstore, waar mensen gewend zijn op een knop te drukken. Daar staat tegenover dat een link meteen werkt: stuur je iemand een link naar een pagina, dan opent die in de browser, zonder installatie.

Wat iOS beperkt

Apple past dit regelmatig aan, dus test op echte toestellen en controleer de documentatie van WebKit. Op het moment van schrijven gaat het hierover:

API-sleutels

Alles in een PWA kan de gebruiker downloaden en lezen, dus een API-sleutel in je front-end is een publieke sleutel. Dat geldt ook voor TMDB. Een kleine server ertussen lost dat op en kan meteen antwoorden cachen, en met Cloudflare is een Worker daar een voor de hand liggende plek voor.

Wat je wint en wat je inlevert

Je hebt één codebase voor iPhone, Android en desktop. Je zet een update online zonder keuring, en je gebruikers moeten daarvoor niets installeren. Elke pagina heeft een link die je kan delen.

Met alleen een PWA sta je niet in de appstore, waar mensen apps gaan zoeken. Op iOS heb je de beperkingen hierboven. Voor toegang tot het toestel ben je afhankelijk van wat de browser aanbiedt, en een native app heeft meer toegang. Hoe je die afweging voor een bedrijf maakt, staat in Heb je de appstore wel nodig?

Bij ReadyToBinge staat daarom ook een native app in de winkels, naast de PWA.

Lees ook

Wat is jouw idee?

Vertel me wat je wil bouwen. Ik probeer binnen één werkdag te antwoorden.