2HAASStart a project
All posts

Guides

Build Offline Web Apps with Service Workers

Discover how to use Service Workers to make your web applications work offline. Learn caching strategies and PWA principles.

The 2HAAS team5 min read

Modern web applications promise rich, dynamic experiences, but their reliability often crumbles with an unstable or absent internet connection. Users expect instant access and seamless functionality, regardless of network availability. Building an application that remains functional offline is a significant challenge for many developers, leading to frustrating user experiences and lost engagement when connectivity drops.

Understanding Service Workers

Service Workers are specialized JavaScript files that act as a programmable proxy between your web application and the network. They run in the background, separate from the main browser thread, giving them powerful capabilities like intercepting network requests, caching assets, and delivering push notifications. Crucially, they enable "offline-first" architectures, allowing your application to function even when the user has no internet connection. They are a fundamental building block for Progressive Web Apps (PWAs).

Service Workers follow a lifecycle that includes registration, installation, and activation. During the installation phase, the Service Worker can pre-cache essential resources. In the activation phase, it takes control of the page and can clean up old caches. After activation, it can intercept fetch requests, serving content from cache or the network as needed.

Registering Your First Service Worker

To get started, you first need to register your Service Worker file from your main application script. The Service Worker script itself is typically placed in the root directory of your project to maximize its scope.

Here is how you register a Service Worker:

  1. Create your Service Worker file: Name it sw.js (or similar) and place it at the root of your project.

  2. Add registration code to your main JavaScript file:

    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
          .then(registration => {
            console.log('Service Worker registered with scope:', registration.scope);
          })
          .catch(error => {
            console.error('Service Worker registration failed:', error);
          });
      });
    }
    

    This code checks if Service Workers are supported by the browser. If so, it attempts to register /sw.js when the page finishes loading. The scope of /sw.js will be the directory it's in and all its subdirectories.

Implementing Caching Strategies

The core of an offline-capable application lies in its caching strategy. Service Workers use the Cache Storage API to store network responses.

Let's look at a common "Cache First, then Network" strategy for static assets and "Network First, then Cache" for dynamic content.

Cache First Strategy (for App Shell/Static Assets)

This strategy serves assets directly from the cache if available. If not, it falls back to the network and caches the response for future use. This is ideal for your application's shell (HTML, CSS, JavaScript, images) which rarely changes.

  1. Define static assets to cache during installation: In sw.js:

    const CACHE_NAME = 'my-app-cache-v1';
    const urlsToCache = [
      '/',
      '/index.html',
      '/styles/main.css',
      '/scripts/app.js',
      '/images/logo.png'
    ];
    
    self.addEventListener('install', event => {
      console.log('Service Worker: Install event');
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then(cache => {
            console.log('Service Worker: Caching app shell');
            return cache.addAll(urlsToCache);
          })
      );
    });
    
  2. Intercept fetch requests and serve from cache: In sw.js:

    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request)
          .then(response => {
            // Cache hit - return response
            if (response) {
              return response;
            }
            // No cache hit - fetch from network
            return fetch(event.request).then(
              networkResponse => {
                // Check if we received a valid response
                if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') {
                  return networkResponse;
                }
                // Clone the response because it's a stream and can only be consumed once
                const responseToCache = networkResponse.clone();
                caches.open(CACHE_NAME)
                  .then(cache => {
                    cache.put(event.request, responseToCache);
                  });
                return networkResponse;
              }
            );
          })
      );
    });
    

Network First Strategy (for Dynamic Content)

This strategy tries to fetch content from the network first. If the network fails, it falls back to the cache. This is suitable for dynamic data that needs to be as fresh as possible, but still needs offline availability.

// Inside your fetch event listener in sw.js, you might conditionally apply this:
// Example for an API call
if (event.request.url.startsWith('https://api.example.com/')) {
  event.respondWith(
    fetch(event.request)
      .then(networkResponse => {
        // Cache the new network response
        return caches.open(CACHE_NAME).then(cache => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
      })
      .catch(() => {
        // Network failed, try to serve from cache
        return caches.match(event.request);
      })
  );
}

Handling Updates and Versioning

Cache management is crucial for keeping your application fresh. When you update your application, you typically want to update your cached assets.

  1. Update CACHE_NAME: Increment the version number (e.g., my-app-cache-v2).

  2. Cleanup old caches during activation: In sw.js:

    self.addEventListener('activate', event => {
      console.log('Service Worker: Activate event');
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames.map(cacheName => {
              if (cacheName !== CACHE_NAME) {
                console.log('Service Worker: Deleting old cache:', cacheName);
                return caches.delete(cacheName);
              }
            })
          );
        })
      );
    });
    

    This ensures that when a new Service Worker activates, it removes any old caches, preventing stale content from being served.

Best Practices for Offline Apps

  • HTTPS is mandatory: Service Workers only operate over HTTPS (except for localhost during development) for security reasons.
  • Keep sw.js lean: Avoid heavy computations in the Service Worker file itself. Its primary job is request interception and caching.
  • Test thoroughly: Use browser developer tools (Application tab) to inspect Service Worker status, caches, and network requests. Simulate offline mode.
  • Provide user feedback: Inform users when they are offline or when new content is available and needs a refresh.
  • Consider Workbox: For more complex PWA needs, libraries like Google's Workbox provide a higher-level abstraction for Service Worker development, simplifying caching strategies and build processes.

Takeaway

Embracing Service Workers transforms your web application into a robust, reliable experience that thrives even without a network connection. By strategically caching resources and managing updates, you can significantly improve user satisfaction and engagement. Start small, register your Service Worker, implement a basic cache, and then expand your offline capabilities.

Keep reading

  1. Guides5 min read

    Guide to Implementing Web Push Notifications

    Learn how to set up and send web push notifications to re-engage users. This guide covers client-side setup and server-side logic.

  2. Guides4 min read

    Mastering Responsive Layouts with CSS Container Queries

    Learn how to implement CSS Container Queries for truly responsive, component-based layouts. This guide covers setup, syntax, and practical examples.

  3. Tech News5 min read

    TypeScript 5.5 Beta Enhances Developer Experience

    Learn about the new features in TypeScript 5.5 Beta. Discover how improved control flow analysis and type inference enhance your coding.

Tell us what’s in the way.

A few lines is enough. You get a candid yes, a candid no, or a sharper question.

  • Working software in week four
  • A reply in one business day
  • Handover is the deliverable

Prefer email? hello@2haas.com

The problem, who it is for, and what is in the way right now.

One business day to a real reply. No sales sequence, no call required.