Back to blog

PWA in 2025: Why Developing a Web App Instead of Native Is Becoming a Trend

Hello HaWkers, while companies spend millions developing separate native apps for iOS and Android, a silent but powerful trend is transforming mobile development: Progressive Web Apps (PWAs) are allowing the creation of a single web application that works perfectly both in the browser and installed as a native app.

Companies that have adopted PWAs report 50%+ improvements in user engagement, drastic cost reduction, and elimination of the complexity of maintaining multiple codebases. In 2025, the question is no longer "should I create a PWA?" but "why haven't I created one yet?".

Let's understand what PWAs are, their real benefits, and how you can implement a modern PWA that competes with native apps.

The Problem with Traditional Mobile Development

In the traditional mobile app model, you develop separate projects:

  • iOS: Swift/Objective-C with Xcode
  • Android: Kotlin/Java with Android Studio
  • Web: JavaScript/TypeScript with web frameworks
// Traditional reality
const developmentCosts = {
  iosTeam: 3, // iOS developers
  androidTeam: 3, // Android developers
  webTeam: 2, // web developers
  totalDevelopers: 8,
  monthlyCostPerDev: 10000, // USD
  totalMonthlyCost: 8 * 10000 // = $80,000/month
};

// Not counting:
// - Tripled development time
// - Different bugs on each platform
// - Unsynchronized features
// - Complex maintenance

Problems with this approach:

  1. Very high cost: Multiple teams and codebases
  2. Development time: 3x slower
  3. Fragmentation: Different features on each platform
  4. Store approval: Delay and rejection risks
  5. Slow updates: Users need to download updates

What Are Progressive Web Apps (PWAs)

PWAs are web applications that use modern technologies to offer an experience similar to native apps, but running in the browser and able to be "installed" without app stores.

Main Features:

  1. Installable: Can be added to home screen
  2. Offline-first: Works without internet
  3. Push notifications: Notifications like native app
  4. Fast: Performance close to native apps
  5. Responsive: Works on any device
  6. Secure: Requires HTTPS
// PWA vs Native App
const pwaApproach = {
  platforms: 'Web (works everywhere)',
  languages: 'JavaScript/TypeScript',
  distribution: 'Web (no app stores)',
  updates: 'Instant (refresh)',
  development: 'One codebase',
  cost: '~$20,000/month' // 75% savings!
};

PWA installation

Service Workers: The Heart of PWA

Service Workers are scripts that run in background and intercept network requests, enabling cache, offline, and push notifications.

// service-worker.js
const CACHE_NAME = 'my-pwa-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/images/logo.png'
];

// Install: Cache essential resources
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Cache opened');
        return cache.addAll(urlsToCache);
      })
  );
});

// Fetch: Intercept requests
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Cache hit - return from cache
        if (response) {
          return response;
        }

        // Cache miss - fetch from network
        return fetch(event.request).then(response => {
          // Don't cache if not 200
          if (!response || response.status !== 200 || response.type !== 'basic') {
            return response;
          }

          // Clone response and add to cache
          const responseToCache = response.clone();
          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });

          return response;
        });
      })
  );
});

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

Manifest: Making PWA Installable

The Web App Manifest is a JSON file that defines how the PWA appears when installed:

// manifest.json
{
  "name": "My Awesome App",
  "short_name": "App",
  "description": "A complete PWA with offline and notifications",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "orientation": "portrait-primary",
  "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-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "shortcuts": [
    {
      "name": "New Post",
      "short_name": "New",
      "description": "Create a new post",
      "url": "/new-post",
      "icons": [{ "src": "/icons/new.png", "sizes": "96x96" }]
    }
  ],
  "categories": ["productivity", "social"]
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="theme-color" content="#007bff">
  <link rel="manifest" href="/manifest.json">
  <link rel="icon" href="/favicon.ico">
  <title>My PWA</title>
</head>
<body>
  <div id="app"></div>

  <script>
    // Register Service Worker
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
          .then(registration => {
            console.log('SW registered:', registration);
          })
          .catch(error => {
            console.log('SW registration failed:', error);
          });
      });
    }
  </script>
</body>
</html>

Push Notifications: Engagement Like Native App

PWAs can send push notifications even when the browser is closed:

// client-side: Request permission and get subscription
async function subscribeToPush() {
  // Request permission
  const permission = await Notification.requestPermission();

  if (permission !== 'granted') {
    console.log('Notification permission denied');
    return;
  }

  // Get service worker registration
  const registration = await navigator.serviceWorker.ready;

  // Subscribe to push
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
  });

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

  console.log('Push subscription successful');
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}
// service-worker.js: Receive and display push notification
self.addEventListener('push', event => {
  const data = event.data.json();

  const options = {
    body: data.body,
    icon: '/icons/icon-192x192.png',
    badge: '/icons/badge-72x72.png',
    vibrate: [100, 50, 100],
    data: {
      url: data.url
    },
    actions: [
      { action: 'open', title: 'Open' },
      { action: 'close', title: 'Close' }
    ]
  };

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

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

  if (event.action === 'open') {
    event.waitUntil(
      clients.openWindow(event.notification.data.url)
    );
  }
});

PWA with Next.js: Modern Setup

Next.js makes PWA creation much easier with the next-pwa plugin:

npm install next-pwa
// next.config.js
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === 'development'
});

module.exports = withPWA({
  reactStrictMode: true,
  // Other Next.js configs
});
// app/layout.tsx
export const metadata = {
  manifest: '/manifest.json',
  themeColor: '#007bff',
  viewport: 'width=device-width, initial-scale=1, maximum-scale=5',
  appleWebApp: {
    capable: true,
    statusBarStyle: 'default',
    title: 'My PWA'
  }
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="manifest" href="/manifest.json" />
        <meta name="theme-color" content="#007bff" />
      </head>
      <body>{children}</body>
    </html>
  );
}

Advanced Cache Strategies

Different strategies for different content types:

// service-worker.js - Cache strategies
const CACHE_NAME = 'my-pwa-v1';

// Cache-First: Static assets (images, CSS, JS)
const cacheFirst = async (request) => {
  const cached = await caches.match(request);
  return cached || fetch(request);
};

// Network-First: Dynamic content (API calls)
const networkFirst = async (request) => {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch (error) {
    return caches.match(request);
  }
};

// Stale-While-Revalidate: Best of both
const staleWhileRevalidate = async (request) => {
  const cached = await caches.match(request);

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

  return cached || fetchPromise;
};

self.addEventListener('fetch', event => {
  const { request } = event;
  const url = new URL(request.url);

  // Static assets: Cache-First
  if (request.destination === 'image' || request.destination === 'style') {
    event.respondWith(cacheFirst(request));
  }
  // API calls: Network-First
  else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
  }
  // HTML: Stale-While-Revalidate
  else {
    event.respondWith(staleWhileRevalidate(request));
  }
});

Real Success Cases

Companies that adopted PWAs and their results:

Twitter Lite (PWA):

  • 65% increase in pages per session
  • 75% increase in Tweets sent
  • 20% reduction in bounce rate
  • 3 seconds initial load

Pinterest PWA:

  • 60% increase in engagement
  • 44% increase in ad revenue
  • 50% increase in session time

Starbucks PWA:

  • 2x increase in daily orders
  • 99.84% smaller than native iOS app
  • Full offline functionality

PWA vs Native App: When to Choose Each

✅ Choose PWA when:

  • Want maximum reach (web + mobile)
  • Need instant updates
  • Budget is limited
  • Team is small or unique
  • Don't need advanced native APIs

❌ Choose Native App when:

  • Need deep hardware access
  • Performance is absolutely critical (3D games)
  • Features like Bluetooth, NFC, ARKit are essential
  • App store presence is mandatory

🔄 Hybrid Approach:

Many companies adopt both: PWA as base + native apps for specific features. PWA reaches everyone, and those who need advanced features download the native app.

Essential PWA Tools

# Lighthouse: Audit PWA
npx lighthouse https://your-site.com --view

# Workbox: Library for Service Workers
npm install workbox-webpack-plugin

# PWA Asset Generator: Generate icons
npm install -g pwa-asset-generator
pwa-asset-generator logo.png ./icons

If you want to understand more about modern web development and performance, I recommend reading: WebAssembly and JavaScript: Web Performance in 2025 where we explore advanced optimization techniques.

Let's go! 🦅

🎯 Master Modern Web Development

This article covered PWAs and mobile development with web technologies, but there is much more to explore about JavaScript and modern frameworks.

Developers who master PWAs and web technologies have access to a huge and growing market.

Start Now

If you want to master JavaScript from scratch to advanced:

Payment options:

  • $4.90 (single payment)

📖 View Complete Content

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments