正在加载,请稍候…

PWA in 2026: Service Workers, Offline-First, and Modern Install Experiences

A complete guide to building Progressive Web Apps in 2026 with advanced Service Worker patterns, offline-first strategies, push notifications, and seamless install experiences.

PWA in 2026: Service Workers, Offline-First, and Modern Install Experiences

Progressive Web Apps have matured significantly. In 2026, PWAs offer capabilities that rival native apps on both Android and iOS. With service workers, background sync, push notifications, and file system access, the line between web and native continues to blur.

The PWA Capability Landscape in 2026

Modern PWAs can access:

  • File System Access API: Read/write files with user permission
  • Web Bluetooth and USB: Connect to hardware devices
  • Background Sync: Queue actions for when connectivity returns
  • Periodic Background Sync: Regular data updates (Android)
  • Web Push: Notifications even when app is closed
  • Badging API: App icon badges
  • Share Target API: Receive shared content from other apps

Web App Manifest Deep Dive

{
  "name": "My Production PWA",
  "short_name": "MyPWA",
  "display": "standalone",
  "display_override": ["window-controls-overlay", "standalone", "browser"],
  "background_color": "#ffffff",
  "theme_color": "#6366f1",
  "screenshots": [
    {
      "src": "/screenshots/desktop.png",
      "sizes": "1280x800",
      "type": "image/png",
      "form_factor": "wide",
      "label": "Desktop view"
    }
  ],
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
  ],
  "file_handlers": [
    {
      "action": "/open-file",
      "accept": { "text/markdown": [".md"] }
    }
  ],
  "share_target": {
    "action": "/share-target",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": { "title": "title", "text": "text", "url": "url" }
  }
}

Advanced Service Worker Patterns

Workbox 7.x Configuration

import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { BroadcastUpdatePlugin } from 'workbox-broadcast-update';

precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();

// HTML - Network First
registerRoute(
  ({ request }) => request.destination === 'document',
  new NetworkFirst({
    cacheName: 'html-cache',
    networkTimeoutSeconds: 3,
    plugins: [new ExpirationPlugin({ maxAgeSeconds: 24 * 60 * 60 })],
  })
);

// API - Network First with update notification
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 5,
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 5 * 60 }),
      new BroadcastUpdatePlugin(),
    ],
  })
);

// Images - Cache First, long expiry
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }),
    ],
  })
);

Background Sync

const bgSyncPlugin = new BackgroundSyncPlugin('form-queue', {
  maxRetentionTime: 24 * 60,
  onSync: async ({ queue }) => {
    let entry;
    while ((entry = await queue.shiftRequest())) {
      try {
        await fetch(entry.request);
      } catch (error) {
        await queue.unshiftRequest(entry);
        throw error;
      }
    }
  },
});

registerRoute(
  ({ url }) => url.pathname === '/api/submit',
  new NetworkOnly({ plugins: [bgSyncPlugin] }),
  'POST'
);

Smart Update Management

class ServiceWorkerManager {
  async register() {
    if (!('serviceWorker' in navigator)) return;

    this.registration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/',
      updateViaCache: 'none',
    });

    setInterval(() => this.registration.update(), 60 * 60 * 1000);

    this.registration.addEventListener('updatefound', () => {
      const newWorker = this.registration.installing;
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          this.onUpdateAvailable?.();
        }
      });
    });

    navigator.serviceWorker.addEventListener('controllerchange', () => {
      window.location.reload();
    });
  }

  skipWaiting() {
    this.registration?.waiting?.postMessage({ type: 'SKIP_WAITING' });
  }
}

Push Notifications

Requesting Permission and Subscribing

async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') throw new Error('Permission denied');

  const sub = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
  });

  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(sub.toJSON()),
  });

  return sub;
}

Handling Push in Service Worker

self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  event.waitUntil(
    self.registration.showNotification(data.title ?? 'New Update', {
      body: data.body,
      icon: '/icons/icon-192.png',
      badge: '/icons/badge-72.png',
      data: { url: data.url },
      actions: [
        { action: 'open', title: 'Open App' },
        { action: 'dismiss', title: 'Dismiss' },
      ],
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  if (event.action === 'dismiss') return;

  const url = event.notification.data?.url ?? '/';
  event.waitUntil(
    clients.matchAll({ type: 'window' }).then((clientList) => {
      const existing = clientList.find(c => c.url === url);
      if (existing) return existing.focus();
      return clients.openWindow(url);
    })
  );
});

Custom Install Prompt

class PWAInstallManager {
  private deferredPrompt = null;

  constructor() {
    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault();
      this.deferredPrompt = e;
      this.onInstallAvailable?.();
    });

    window.addEventListener('appinstalled', () => {
      this.deferredPrompt = null;
      analytics.track('pwa_installed');
    });
  }

  get canInstall() { return !!this.deferredPrompt; }

  async promptInstall() {
    if (!this.deferredPrompt) return null;
    this.deferredPrompt.prompt();
    const { outcome } = await this.deferredPrompt.userChoice;
    this.deferredPrompt = null;
    return outcome;
  }
}

Periodic Background Sync

// Register periodic sync
async function registerPeriodicSync() {
  const registration = await navigator.serviceWorker.ready;
  if ('periodicSync' in registration) {
    try {
      await registration.periodicSync.register('update-feed', {
        minInterval: 24 * 60 * 60 * 1000, // 24 hours
      });
    } catch (e) {
      console.log('Periodic sync registration failed:', e);
    }
  }
}

// Handle in service worker
self.addEventListener('periodicsync', (event) => {
  if (event.tag === 'update-feed') {
    event.waitUntil(updateFeedCache());
  }
});

Conclusion

PWAs in 2026 are genuinely capable cross-platform applications. With advanced Service Worker strategies, background sync, push notifications, and thoughtful install experiences, you can deliver app-store-quality experiences through the web. The investment in PWA architecture pays dividends across all platforms simultaneously.