Guides

Building Mobile Web / PWA with open-source tools

This guide provides a technical roadmap for converting a standard web application into a production-ready Progressive Web App (PWA). We focus on implementing resilient offline support using Workbox, optimizing manifest configurations for cross-platform installs, and handling the specific lifecycle events required for a seamless mobile user experience.

3 hours6 steps
1

Configure the Web App Manifest

Create a `manifest.webmanifest` file to define how your app appears on the mobile OS. You must provide at least two icons (192x192 and 512x512) and set the `display` property to `standalone` to remove browser UI elements.

public/manifest.webmanifest
{
  "name": "Pro Mobile App",
  "short_name": "ProApp",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
    { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512", "purpose": "any maskable" }
  ]
}

⚠ Common Pitfalls

  • Missing 'maskable' icons will cause unsightly borders on Android devices.
  • iOS ignores the theme_color property in the manifest; use the meta tag <meta name='apple-mobile-web-app-status-bar-style' content='black-translucent'> instead.
2

Integrate Vite PWA Plugin for Service Worker Management

Use `vite-plugin-pwa` to automate service worker generation. This handles the 'GenerateSW' or 'InjectManifest' strategies, ensuring your static assets are automatically precached during the build process.

vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg}']
      }
    })
  ]
});

⚠ Common Pitfalls

  • Setting registerType to 'autoUpdate' may interrupt user sessions by reloading the app; consider 'prompt' for complex applications.
  • Ensure the service worker file is served from the root directory to maximize its scope.
3

Implement Runtime Caching for Dynamic Routes

Configure Workbox runtime caching to handle API requests and external resources. Use the 'StaleWhileRevalidate' strategy for assets that need to be fast but can afford to be slightly out of date, and 'NetworkFirst' for critical data.

vite.config.ts
workbox: {
  runtimeCaching: [{
    urlPattern: /^https:\/\/api\.example\.com\/v1\/.*$/,
    handler: 'NetworkFirst',
    options: {
      cacheName: 'api-cache',
      expiration: {
        maxEntries: 50,
        maxAgeSeconds: 86400
      }
    }
  }]
}

⚠ Common Pitfalls

  • Caching POST requests is not supported by default and requires custom handling in the fetch event.
  • Large cache sizes can lead to 'QuotaExceeded' errors on mobile devices with low storage.
4

Handle the Custom Install Prompt

Browsers like Chrome trigger a `beforeinstallprompt` event. Capture this event to provide a custom UI button for installation, as the default browser banner is often suppressed or ignored by users.

install-handler.js
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  showInstallButton();
});

async function installApp() {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    deferredPrompt = null;
  }
}

⚠ Common Pitfalls

  • The beforeinstallprompt event is currently not supported on iOS Safari.
  • The event will only fire if the PWA meets all installability criteria (HTTPS, valid manifest, service worker with fetch handler).
5

Add an Offline Fallback Page

When a user is offline and attempts to navigate to a page not in the cache, the app should serve a custom `offline.html` instead of the browser's default error page. This ensures the app feels like a native experience even without connectivity.

vite.config.ts
workbox: {
  offlineGoogleAnalytics: true,
  runtimeCaching: [{
    handler: 'NetworkOnly',
    urlPattern: /\/api\/.*$/,
    options: {
      precacheFallback: {
        fallbackURL: '/offline.html'
      }
    }
  }]
}

⚠ Common Pitfalls

  • Failing to precache the offline.html file itself will result in a 404 when the user is actually offline.
  • Ensure the offline page has minimal external dependencies (CSS/JS) or that they are also precached.
6

Optimize Performance for Mobile Hardware

PWAs are often run on devices with limited CPU. Implement content-visibility: auto for long lists, use SVGs over heavy PNGs, and ensure all touch targets are at least 44x44px to comply with accessibility and mobile usability standards.

styles.css
.list-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 50px;
  min-height: 48px;
  padding: 12px;
  touch-action: manipulation;
}

⚠ Common Pitfalls

  • Overusing heavy JavaScript frameworks can lead to high 'Time to Interactive' on mid-range Android devices.
  • Not using 'touch-action: manipulation' can cause a 300ms delay on older mobile browsers.

What you built

By following these steps, you have moved beyond a simple website to a resilient PWA. Your application now supports offline access, features a platform-integrated installation flow, and is optimized for the hardware constraints of mobile devices. Continuous monitoring via Lighthouse and WebPageTest is recommended to ensure the service worker and caching strategies remain performant as the app grows.