Qué es una PWA y por que te importa

Una Progressive Web App es una aplicación web que ofrece una experiencia similar a una app nativa: se puede instalar en el dispositivo, funciona offline y tiene acceso a APIs del sistema como notificaciones push y compartir contenido.

En Latinoamerica, las PWAs son especialmente relevantes por dos razones:

  1. Conectividad inestable: Muchos usuarios tienen conexiones lentas o intermitentes. El soporte offline no es un lujo, es una necesidad
  2. Dispositivos de gama media-baja: Las PWAs ocupan una fraccion del espacio de una app nativa, no necesitan descargarse desde una tienda y se actualizan automaticamente

Prerequisitos

Para seguir esta guia necesitas:

  • Angular 21+ (funciona desde Angular 6, pero usaremos APIs modernas)
  • Node.js 22+
  • Un proyecto Angular existente
  • HTTPS en producción (los service workers requieren HTTPS)

Paso 1: Agregar soporte PWA

Angular CLI tiene un schematic dedicado que configura todo lo necesario. Ejecuta el comando del primer bloque de código y el schematic hará el trabajo pesado por ti.

Qué archivos genera

Despues de ejecutar ng add @angular/pwa, tu proyecto tendrá:

  • ngsw-config.json: Configuración del service worker
  • src/manifest.webmanifest: Manifiesto de la PWA
  • Iconos placeholder en src/assets/icons/
  • Referencias actualizadas en index.html y app.config.ts

Paso 2: Configurar el manifiesto

El archivo manifest.webmanifest define como se ve y se comporta tu app cuando se instala:

{
  "name": "Bemore Learn - Plataforma de Aprendizaje",
  "short_name": "Bemore Learn",
  "description": "Aprende desarrollo web con cursos, tutoriales y una comunidad activa",
  "theme_color": "#0a0a0f",
  "background_color": "#0a0a0f",
  "display": "standalone",
  "orientation": "any",
  "scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ],
  "screenshots": [
    {
      "src": "assets/screenshots/desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "assets/screenshots/mobile.png",
      "sizes": "750x1334",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ]
}

Campos clave

  • display: standalone: La app se ve como nativa (sin barra del navegador)
  • theme_color: Color de la barra de estado en Android
  • icons con purpose maskable: Necesarios para adaptive icons en Android
  • screenshots: Mejoran la experiencia de instalación en Chrome y Edge

Paso 3: Configurar el Service Worker

El archivo ngsw-config.json es donde defines que recursos cachear y como. Revisa el segundo bloque de código para ver la configuración completa.

Asset Groups

Hay dos estrategias de instalación:

prefetch: Los archivos se descargan inmediatamente cuando el service worker se instala. Usa esto para el app shell (HTML, CSS, JS principal).

lazy: Los archivos se cachean solo cuando se solicitan por primera vez. Usa esto para assets como imagenes y fuentes que el usuario puede no necesitar inmediatamente.

Data Groups

Para datos dinámicos (APIs), tienes dos estrategias:

freshness: Intenta obtener datos frescos del servidor. Si falla (timeout o sin conexión), usa la cache. Ideal para datos que cambian frecuentemente.

performance: Usa la cache primero. Solo va al servidor si la cache esta vacia o expirada. Ideal para datos que cambian poco (contenido estático, configuraciones).

Paso 4: Manejo de actualizaciones

Cuando despliegas una nueva versión de tu app, el service worker la detecta y la descarga en segundo plano. Pero necesitas notificar al usuario para que recargue y obtenga la versión nueva.

El tercer bloque de código muestra un servicio que escucha actualizaciones y pregunta al usuario si quiere actualizar.

Integracion en app.config.ts

Asegurate de que el service worker este registrado correctamente:

import { provideServiceWorker } from '@angular/service-worker';
import { isDevMode } from '@angular/core';

export const appConfig = {
  providers: [
    provideServiceWorker('ngsw-worker.js', {
      enabled: !isDevMode(),
      registrationStrategy: 'registerWhenStable:30000',
    }),
  ],
};

La opcion registerWhenStable:30000 espera hasta que la app este estable o hasta 30 segundos, lo que ocurra primero. Esto evita que el service worker compita con la carga inicial de la app.

Paso 5: Instalacion de la PWA

Para ofrecer una experiencia de instalación personalizada, puedes capturar el evento beforeinstallprompt:

import { Injectable, signal } from '@angular/core';

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

@Injectable({ providedIn: 'root' })
export class PwaInstallService {
  readonly canInstall = signal(false);
  private deferredPrompt: BeforeInstallPromptEvent | null = null;

  constructor() {
    window.addEventListener('beforeinstallprompt', (event) => {
      event.preventDefault();
      this.deferredPrompt = event as BeforeInstallPromptEvent;
      this.canInstall.set(true);
    });

    window.addEventListener('appinstalled', () => {
      this.canInstall.set(false);
      this.deferredPrompt = null;
    });
  }

  async install(): Promise<boolean> {
    if (!this.deferredPrompt) return false;

    await this.deferredPrompt.prompt();
    const result = await this.deferredPrompt.userChoice;
    this.deferredPrompt = null;

    if (result.outcome === 'accepted') {
      this.canInstall.set(false);
      return true;
    }
    return false;
  }
}

Paso 6: Soporte offline

Página offline personalizada

Cuando el usuario no tiene conexión y navega a una página no cacheada, puedes mostrar una página offline personalizada en lugar del dinosaurio de Chrome:

<!-- src/offline.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <title>Sin conexión - Bemore Learn</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      margin: 0;
      background: #0a0a0f;
      color: #f0f0f5;
    }
    .container { text-align: center; padding: 2rem; }
    h1 { font-size: 1.5rem; margin-bottom: 1rem; }
    p { color: #a0a0b8; }
    button {
      margin-top: 1.5rem;
      padding: 0.75rem 1.5rem;
      background: #ff530f;
      color: white;
      border: none;
      border-radius: 0.5rem;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Sin conexión a internet</h1>
    <p>Parece que no tienes conexión. Las páginas que visitaste
    anteriormente siguen disponibles offline.</p>
    <button onclick="location.reload()">Reintentar</button>
  </div>
</body>
</html>

Paso 7: Testing de la PWA

En desarrollo

Los service workers no funcionan en modo desarrollo. Para probar:

ng build --configuration production
npx http-server dist/mi-app/browser -p 8080

Checklist de validación

Usa Lighthouse en Chrome DevTools para auditar tu PWA. Apunta a:

  • Performance: 90+
  • PWA: Todos los checks en verde
  • Accesibilidad: 90+
  • Best Practices: 90+

Criterios para ser instalable

Chrome requiere estos minimos para mostrar el prompt de instalación:

  • Servido con HTTPS
  • Tiene manifest con name, icons (192px y 512px), start_url y display
  • Tiene service worker registrado con un fetch handler
  • No esta ya instalada

Optimizaciones avanzadas

Precargar rutas críticas

En tu ngsw-config.json, agrega las URLs que quieres precachear:

{
  "assetGroups": [
    {
      "name": "routes",
      "installMode": "prefetch",
      "resources": {
        "urls": [
          "/",
          "/cursos",
          "/blog"
        ]
      }
    }
  ]
}

Background sync

Para funcionalidad offline avanzada (como guardar progreso sin conexión), usa Background Sync:

if ('serviceWorker' in navigator && 'SyncManager' in window) {
  const registration = await navigator.serviceWorker.ready;
  await (registration as unknown as { sync: { register: (tag: string) => Promise<void> } })
    .sync.register('sync-progress');
}

Push notifications

Las notificaciones push requieren un servidor de push y la API Push del navegador. Es un tema completo en si mismo, pero el setup básico con Firebase Cloud Messaging es:

  1. Configurar FCM en la consola de Firebase
  2. Agregar firebase-messaging-sw.js
  3. Solicitar permiso de notificaciones
  4. Registrar el token del dispositivo en tu backend

Generando iconos reales

Los iconos placeholder que genera Angular PWA schematic no sirven para producción. Genera iconos reales:

  1. Crea un icono base de 1024x1024 px en PNG
  2. Usa una herramienta como PWA Asset Generator o RealFaviconGenerator
  3. Genera todos los tamaños necesarios (72, 96, 128, 144, 152, 192, 384, 512)
  4. Reemplaza los placeholders en src/assets/icons/

Conclusion

Convertir una app Angular en PWA es sorprendentemente sencillo gracias al schematic de Angular. Con unas pocas configuraciones tienes una app instalable, con soporte offline y actualizaciones automaticas.

En mercados como Latinoamerica, donde la conectividad es variable y los usuarios dependen de dispositivos móviles, una PWA puede ser la diferencia entre una app que se usa y una que se abandona. Invierte tiempo en configurar correctamente el service worker y el manifiesto; tus usuarios lo notaran.