DEV Community

ahmet gedik
ahmet gedik

Posted on

Building a Video Platform That Works Offline-First

What if your video platform could work even when the user's connection is unreliable? Here's how I implemented offline-first capabilities for TrendVidStream using Service Workers and the Cache API.

Why Offline-First for Video?

Not all our users have stable connections. TrendVidStream serves 8 regions including countries where mobile internet can be intermittent. Offline-first ensures:

  • Previously visited pages load instantly
  • Navigation works even with spotty connectivity
  • The shell (header, categories, footer) always renders

Service Worker Registration

// sw-register.js
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        try {
            const reg = await navigator.serviceWorker.register('/sw.js', {
                scope: '/'
            });
            console.log('SW registered:', reg.scope);
        } catch (err) {
            console.error('SW registration failed:', err);
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Service Worker

// sw.js
const CACHE_NAME = 'tvs-v1';
const SHELL_CACHE = 'tvs-shell-v1';

// App shell resources (always cached)
const SHELL_URLS = [
    '/',
    '/assets/style.css',
    '/assets/app.js',
    '/offline.html',
];

// Install: cache app shell
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(SHELL_CACHE).then((cache) => {
            return cache.addAll(SHELL_URLS);
        })
    );
    self.skipWaiting();
});

// Activate: clean old caches
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys().then((keys) => {
            return Promise.all(
                keys
                    .filter((key) => key !== CACHE_NAME && key !== SHELL_CACHE)
                    .map((key) => caches.delete(key))
            );
        })
    );
    self.clients.claim();
});

// Fetch: stale-while-revalidate for pages, cache-first for assets
self.addEventListener('fetch', (event) => {
    const url = new URL(event.request.url);

    // Skip non-GET requests
    if (event.request.method !== 'GET') return;

    // Skip API calls
    if (url.pathname.startsWith('/api/')) return;

    // Static assets: cache-first
    if (url.pathname.match(/\.(css|js|png|jpg|webp|svg|woff2)$/)) {
        event.respondWith(cacheFirst(event.request));
        return;
    }

    // HTML pages: stale-while-revalidate
    event.respondWith(staleWhileRevalidate(event.request));
});

async function cacheFirst(request) {
    const cached = await caches.match(request);
    if (cached) return cached;

    try {
        const response = await fetch(request);
        if (response.ok) {
            const cache = await caches.open(CACHE_NAME);
            cache.put(request, response.clone());
        }
        return response;
    } catch {
        return new Response('', { status: 503 });
    }
}

async function staleWhileRevalidate(request) {
    const cached = await caches.match(request);

    const fetchPromise = fetch(request)
        .then((response) => {
            if (response.ok) {
                const cache = caches.open(CACHE_NAME);
                cache.then((c) => c.put(request, response.clone()));
            }
            return response;
        })
        .catch(() => null);

    // Return cached version immediately if available
    if (cached) {
        // Update cache in background
        fetchPromise;
        return cached;
    }

    // No cache: wait for network
    const response = await fetchPromise;
    if (response) return response;

    // No cache + no network: show offline page
    return caches.match('/offline.html');
}
Enter fullscreen mode Exit fullscreen mode

Offline Page

<!-- offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Offline - TrendVidStream</title>
    <link rel="stylesheet" href="/assets/style.css">
</head>
<body>
    <header class="vw-header">
        <a href="/" class="logo">TrendVidStream</a>
    </header>
    <main class="offline-message">
        <h1>You're Offline</h1>
        <p>It looks like you've lost your internet connection. Previously visited pages may still be available.</p>
        <p>Try:</p>
        <ul>
            <li><a href="/">Home Page</a></li>
            <li><a href="/category/music">Music</a></li>
            <li><a href="/category/gaming">Gaming</a></li>
        </ul>
    </main>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Cache Management in PHP

<?php

// Generate cache version based on deploy
function getCacheVersion(): string
{
    $versionFile = __DIR__ . '/version.txt';
    if (file_exists($versionFile)) {
        return trim(file_get_contents($versionFile));
    }
    return 'v1';
}

// Add version to asset URLs for cache busting
function asset(string $path): string
{
    $version = getCacheVersion();
    return "/assets/{$path}?v={$version}";
}
Enter fullscreen mode Exit fullscreen mode

This offline-first approach ensures TrendVidStream provides a reliable experience even on unstable connections. The stale-while-revalidate strategy for pages means users see content instantly while fresh data loads in the background.

Top comments (0)