Przejdź do treści
Mobile

Progressive Web Apps in 2025 - Complete Guide

Published on:
·
Updated on:
·7 min read·Author: MDS Software Solutions Group

Progressive Web Apps

mobile

Progressive Web Apps in 2025 - Complete Guide

Progressive Web Apps (PWA) are web applications that combine the best features of websites and native apps. They run in the browser but offer capabilities known from mobile applications: offline functionality, push notifications, home screen installation, and smooth animations. In 2025, PWAs have become the standard for building modern applications that are fast, reliable, and engaging.

What Are Progressive Web Apps?#

PWAs are web applications that meet specific technical and quality criteria. The term was introduced by Google in 2015, but since then, the technology has undergone tremendous evolution. The key characteristics of PWAs include:

  • Progressive - work for every user regardless of browser choice
  • Responsive - adapt to any device (desktop, tablet, phone)
  • Network independent - work offline or with poor connectivity
  • App-like - native-style interactions and navigation
  • Fresh - always up to date thanks to Service Workers
  • Secure - served over HTTPS
  • Discoverable - identified as applications through the W3C manifest
  • Installable - can be installed without an app store
  • Linkable - easily shareable via URL

Service Workers - The Heart of PWA#

A Service Worker is a JavaScript script that runs in the background, independently of the page. It is responsible for offline functionality, background synchronization, and push notifications. It acts as a proxy between the browser and the network.

Registering a Service Worker#

// app/register-sw.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/',
      });
      console.log('SW registered:', registration.scope);

      // Check for updates
      registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'activated') {
            console.log('New version available!');
          }
        });
      });
    } catch (error) {
      console.error('SW registration failed:', error);
    }
  });
}

Service Worker Lifecycle#

A Service Worker goes through several states:

  1. Installation (install) - downloading and caching assets
  2. Waiting (waiting) - waiting for old tabs to close
  3. Activation (activate) - cleaning up old caches
  4. Fetch (fetch) - intercepting network requests
// sw.js
const CACHE_NAME = 'pwa-cache-v1';
const STATIC_ASSETS = [
  '/',
  '/offline.html',
  '/css/styles.css',
  '/js/app.js',
  '/images/logo.webp',
];

// Installation - cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('Cache opened');
      return cache.addAll(STATIC_ASSETS);
    })
  );
  self.skipWaiting();
});

// Activation - clean up old caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
  self.clients.claim();
});

Web App Manifest#

The manifest is a JSON file that tells the browser about your application and determines how it should behave when installed on the user's device.

{
  "name": "My PWA Application",
  "short_name": "MyPWA",
  "description": "An example Progressive Web App",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0070f3",
  "orientation": "portrait-primary",
  "scope": "/",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "/screenshots/mobile.png",
      "sizes": "375x812",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ],
  "shortcuts": [
    {
      "name": "New Post",
      "short_name": "New",
      "url": "/new",
      "icons": [{ "src": "/icons/shortcut-new.png", "sizes": "96x96" }]
    }
  ]
}

Adding the manifest to your page:

<head>
  <link rel="manifest" href="/manifest.json" />
  <meta name="theme-color" content="#0070f3" />
  <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
</head>

Caching Strategies - The Offline-First Approach#

Choosing the right caching strategy is crucial for performance and user experience. Here are the most important strategies:

Cache First (Cache, Falling Back to Network)#

Checks the cache first, and if the resource does not exist, fetches from the network. Ideal for static assets that rarely change.

// Cache First Strategy
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        return cachedResponse;
      }
      return fetch(event.request).then((response) => {
        // Save new resource to cache
        if (response.status === 200) {
          const responseClone = response.clone();
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, responseClone);
          });
        }
        return response;
      });
    })
  );
});

Network First (Network, Falling Back to Cache)#

Tries to fetch from the network first, and if that fails, returns the cached version. Ideal for dynamic content such as API responses.

// Network First Strategy
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // Update cache with fresh data
        const responseClone = response.clone();
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, responseClone);
        });
        return response;
      })
      .catch(() => {
        return caches.match(event.request).then((cachedResponse) => {
          return cachedResponse || caches.match('/offline.html');
        });
      })
  );
});

Stale While Revalidate#

Returns the cached version immediately (if available) while fetching an update from the network in the background. Ideal for resources that should be quickly available but also stay current.

// Stale While Revalidate Strategy
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchPromise = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return cachedResponse || fetchPromise;
      });
    })
  );
});

Strategy Comparison#

| Strategy | Speed | Data Freshness | Use Case | |----------|-------|----------------|----------| | Cache First | Very fast | May be stale | Fonts, images, CSS | | Network First | Slower | Always fresh | APIs, dynamic data | | Stale While Revalidate | Fast | Updated in background | Article lists, profiles |

Push Notifications#

Push notifications allow you to engage users even when the application is not open. They require a Service Worker and the Push API.

Subscribing to Notifications#

// Subscribe to push notifications
async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;

  // Check permissions
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') {
    console.log('User denied notifications');
    return;
  }

  // Create subscription
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
  });

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

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');
  const rawData = window.atob(base64);
  return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}

Handling Notifications in the Service Worker#

// sw.js - push handling
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  const options = {
    body: data.body || 'New message!',
    icon: '/icons/icon-192x192.png',
    badge: '/icons/badge-72x72.png',
    vibrate: [200, 100, 200],
    data: { url: data.url || '/' },
    actions: [
      { action: 'open', title: 'Open' },
      { action: 'dismiss', title: 'Dismiss' },
    ],
  };

  event.waitUntil(
    self.registration.showNotification(data.title || 'PWA', options)
  );
});

// Handle notification click
self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  if (event.action === 'dismiss') return;

  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});

PWA Installability#

One of the key advantages of PWAs is the ability to install them without an app store. The browser displays an install button when the application meets PWA criteria.

// Handle beforeinstallprompt event
let deferredPrompt;

window.addEventListener('beforeinstallprompt', (event) => {
  event.preventDefault();
  deferredPrompt = event;

  // Show custom install button
  const installBtn = document.getElementById('install-button');
  installBtn.style.display = 'block';

  installBtn.addEventListener('click', async () => {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log(`User ${outcome === 'accepted' ? 'installed' : 'dismissed'}`);
    deferredPrompt = null;
    installBtn.style.display = 'none';
  });
});

// Detect installation
window.addEventListener('appinstalled', () => {
  console.log('Application installed!');
  deferredPrompt = null;
});

Workbox - Simplifying Service Worker Development#

Workbox is a set of libraries from Google that simplifies Service Worker creation. Instead of writing everything manually, you can use ready-made strategies.

// sw.js with Workbox
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import {
  CacheFirst,
  NetworkFirst,
  StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// Precaching - automatically cache build files
precacheAndRoute(self.__WB_MANIFEST);

// Images - Cache First with 60 image limit, max 30 days
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
      }),
      new CacheableResponsePlugin({ statuses: [0, 200] }),
    ],
  })
);

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

// CSS and JS - Stale While Revalidate
registerRoute(
  ({ request }) =>
    request.destination === 'style' || request.destination === 'script',
  new StaleWhileRevalidate({
    cacheName: 'static-resources',
  })
);

// Google Fonts - Cache First, long TTL
registerRoute(
  ({ url }) => url.origin === 'https://fonts.googleapis.com' ||
    url.origin === 'https://fonts.gstatic.com',
  new CacheFirst({
    cacheName: 'google-fonts',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 30,
        maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
      }),
    ],
  })
);

PWA in Next.js - Step-by-Step Configuration#

Next.js is an excellent choice for building PWAs. Here is how to configure PWA using the next-pwa package:

Installation#

npm install next-pwa

next.config.js Configuration#

// next.config.js
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === 'development',
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
      handler: 'CacheFirst',
      options: {
        cacheName: 'google-fonts',
        expiration: {
          maxEntries: 4,
          maxAgeSeconds: 365 * 24 * 60 * 60,
        },
      },
    },
    {
      urlPattern: /^https:\/\/cdn\.example\.com\/.*/i,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'cdn-assets',
        expiration: {
          maxEntries: 32,
          maxAgeSeconds: 24 * 60 * 60,
        },
      },
    },
    {
      urlPattern: /\/api\/.*$/i,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api-responses',
        expiration: {
          maxEntries: 16,
          maxAgeSeconds: 60 * 60,
        },
        networkTimeoutSeconds: 10,
      },
    },
    {
      urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
      handler: 'CacheFirst',
      options: {
        cacheName: 'static-images',
        expiration: {
          maxEntries: 64,
          maxAgeSeconds: 30 * 24 * 60 * 60,
        },
      },
    },
  ],
});

module.exports = withPWA({
  // Other Next.js configuration
  reactStrictMode: true,
});

Manifest in Next.js (App Router)#

// app/manifest.ts
import type { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: 'My PWA Application',
    short_name: 'MyPWA',
    description: 'A modern Progressive Web App',
    start_url: '/',
    display: 'standalone',
    background_color: '#ffffff',
    theme_color: '#0070f3',
    icons: [
      {
        src: '/icons/icon-192x192.png',
        sizes: '192x192',
        type: 'image/png',
      },
      {
        src: '/icons/icon-512x512.png',
        sizes: '512x512',
        type: 'image/png',
      },
    ],
  };
}

PWA Install Component in React#

// components/InstallPWA.tsx
'use client';

import { useState, useEffect } from 'react';

interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

export function InstallPWA() {
  const [deferredPrompt, setDeferredPrompt] =
    useState<BeforeInstallPromptEvent | null>(null);
  const [isInstalled, setIsInstalled] = useState(false);

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e as BeforeInstallPromptEvent);
    };

    const installedHandler = () => {
      setIsInstalled(true);
      setDeferredPrompt(null);
    };

    window.addEventListener('beforeinstallprompt', handler);
    window.addEventListener('appinstalled', installedHandler);

    // Check if already installed
    if (window.matchMedia('(display-mode: standalone)').matches) {
      setIsInstalled(true);
    }

    return () => {
      window.removeEventListener('beforeinstallprompt', handler);
      window.removeEventListener('appinstalled', installedHandler);
    };
  }, []);

  const handleInstall = async () => {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    if (outcome === 'accepted') {
      setIsInstalled(true);
    }
    setDeferredPrompt(null);
  };

  if (isInstalled || !deferredPrompt) return null;

  return (
    <button
      onClick={handleInstall}
      className="fixed bottom-4 right-4 bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg hover:bg-blue-700 transition-colors z-50"
    >
      Install Application
    </button>
  );
}

Lighthouse PWA Audit#

Google Lighthouse is a page quality auditing tool that includes a dedicated PWA test. To achieve full compliance, your application must meet the following criteria:

  • Loading speed - the page loads fast enough on a 3G network
  • HTTPS - the application is served over a secure connection
  • Service Worker - registered and controlling the page
  • Manifest - valid manifest file with required fields
  • Icons - icons in the proper sizes (192x192 and 512x512)
  • Theme color - defined theme-color meta tag
  • Offline page - the application displays content even without a network
  • HTTP to HTTPS redirect - automatic redirect
# Lighthouse audit from CLI
npx lighthouse https://your-site.com --view --preset=desktop

# PWA audit only
npx lighthouse https://your-site.com --only-categories=pwa --output=json

You can also use the Lighthouse tab in Chrome DevTools (F12 > Lighthouse > Analyze page load).

PWA vs Native Apps#

| Feature | PWA | Native App | |---------|-----|------------| | Installation | From browser, no store needed | Requires store (App Store / Google Play) | | Size | A few KB - MB | Tens to hundreds of MB | | Updates | Automatic | Requires downloading updates | | API access | Limited but growing | Full device API access | | Development cost | Single codebase | Separate code for each platform | | SEO | Full support | None (store visibility only) | | Offline | Yes (Service Worker) | Yes (native storage) | | Push Notifications | Yes (web push) | Yes (native) | | Performance | Good, but lower than native | Highest | | Bluetooth/NFC | Limited (Web Bluetooth API) | Full support |

Browser Support#

In 2025, PWA support is very broad:

  • Chrome - full support on all platforms
  • Edge - full support, Chromium-based
  • Firefox - Service Worker and manifest support (limited installability)
  • Safari/iOS - supported since iOS 16.4, including push notifications and home screen installation
  • Samsung Internet - full support
  • Opera - full support

An important change in 2024/2025: Apple significantly improved PWA support on iOS, adding push notifications in Safari and better support for installable web applications.

Real-World PWA Examples#

Many well-known companies use PWAs with impressive results:

  • Twitter Lite - 65% increase in pages per session, 75% increase in tweets, 20% decrease in bounce rate
  • Pinterest - 60% increase in user engagement, 44% increase in ad revenue
  • Starbucks - PWA is 99.84% smaller than the native iOS app, doubled online orders
  • Uber - the app loads in 3 seconds even on a 2G network
  • Trivago - 150% increase in engagement after PWA implementation

Advanced PWA Features in 2025#

Background Sync#

Background data synchronization when the user regains connectivity:

// Register sync
async function saveData(data) {
  try {
    await fetch('/api/data', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  } catch {
    // Save data locally and register sync
    await saveToIndexedDB('pending-sync', data);
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('sync-data');
  }
}

// sw.js - handle sync
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-data') {
    event.waitUntil(syncPendingData());
  }
});

async function syncPendingData() {
  const pendingData = await getFromIndexedDB('pending-sync');
  for (const data of pendingData) {
    await fetch('/api/data', {
      method: 'POST',
      body: JSON.stringify(data),
    });
    await removeFromIndexedDB('pending-sync', data.id);
  }
}

Periodic Background Sync#

Periodic data synchronization:

// Register periodic sync
const registration = await navigator.serviceWorker.ready;
const status = await navigator.permissions.query({
  name: 'periodic-background-sync',
});

if (status.state === 'granted') {
  await registration.periodicSync.register('update-content', {
    minInterval: 24 * 60 * 60 * 1000, // every 24 hours
  });
}

// sw.js
self.addEventListener('periodicsync', (event) => {
  if (event.tag === 'update-content') {
    event.waitUntil(updateContent());
  }
});

Web Share API#

Native content sharing:

async function shareContent() {
  if (navigator.share) {
    try {
      await navigator.share({
        title: 'My PWA Application',
        text: 'Check out this amazing app!',
        url: window.location.href,
      });
    } catch (err) {
      console.log('Sharing cancelled');
    }
  }
}

PWA Best Practices#

  1. Start with an offline strategy - design your application with offline functionality in mind from the beginning
  2. Optimize cache size - do not cache everything, set limits
  3. Update Service Workers intelligently - inform users about new versions
  4. Use HTTPS - required for Service Workers
  5. Test offline - Chrome DevTools > Application > Service Workers > Offline
  6. Measure performance - regular Lighthouse audits
  7. Load progressively - show a skeleton screen first, then load data
  8. Use the App Shell Pattern - cache the minimal UI and load content dynamically

Conclusion#

Progressive Web Apps in 2025 are a mature technology that offers the best of both worlds: the reach of websites and the experience of native applications. Thanks to Service Workers, Web App Manifest, and modern browser APIs, PWAs enable you to build fast, reliable, and engaging applications that work on every device.

The key to success is choosing the right caching strategies, designing a well-thought-out offline-first architecture, and regularly testing with Lighthouse. Tools like Workbox and framework integrations (e.g., next-pwa) significantly simplify the PWA development process.


Need help building a Progressive Web App or modernizing an existing application? MDS Software Solutions Group specializes in building modern web applications with full PWA support. Contact us to discuss your project and learn how PWA can improve your user engagement.

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

Progressive Web Apps in 2025 - Complete Guide | MDS Software Solutions Group | MDS Software Solutions Group