Published

- 7 min read

Implementa PWA Offline con Service Workers en Next.js sin Dependencias

img of Implementa PWA Offline con Service Workers en Next.js sin Dependencias

Implementa PWA Offline con Service Workers en Next.js sin Dependencias

Implementar funcionalidad offline en una aplicación web moderna puede parecer intimidante, pero no tiene por qué serlo. En este tutorial aprenderás a crear un Service Worker personalizado para Next.js sin usar paquetes externos como next-pwa o next-offline. Tendrás control total sobre el caché y entenderás cada línea de código.

¿Por qué implementar Service Workers manualmente?

1. Control Total

Al escribir tu propio Service Worker, entiendes exactamente cómo funciona el sistema de caché y puedes personalizarlo según tus necesidades específicas.

2. Menos Dependencias

Reduces la superficie de ataque de tu aplicación y evitas problemas de compatibilidad con futuras versiones de Next.js.

3. Aprendizaje Profundo

Dominar los Service Workers te permite implementar patrones avanzados como sincronización en background, notificaciones push y estrategias de caché híbridas.

4. Optimización a Medida

Puedes decidir qué cachear, cuándo invalidar el caché y cómo manejar las actualizaciones sin depender de la configuración de terceros.

Conceptos Fundamentales de Service Workers

Un Service Worker puede estar en 4 estados principales:

  • download: El navegador descarga el archivo del Service Worker
  • install: Se instala y cachea los recursos necesarios
  • waiting: Espera a que se cierren todas las pestañas de la aplicación
  • activate: Se activa y toma control de las peticiones

Evita el Problema del Service Worker que “No se Activa”

Después de desplegar una nueva versión, muchos desarrolladores se frustran porque el Service Worker no se actualiza. Recuerda:

  1. Cierra TODAS las pestañas con tu aplicación (o mejor, cierra el navegador completo)
  2. Un simple Ctrl + R NO es suficiente. Usa hard refresh: Ctrl + Shift + R
  3. Alternativamente, marca “Update on reload” en DevTools > Application > Service Workers

Nuestra Estrategia de Implementación

Vamos a construir una PWA con estas características:

  • Next.js con TypeScript y output: "export" (solo archivos estáticos)
  • Estrategia Cache First: Al instalar guardamos archivos, al activar usamos caché antes que red
  • Red para APIs: Cachearemos archivos estáticos pero permitiremos peticiones dinámicas
  • Versionado automático: El caché se limpia cuando cambia la versión en package.json
  • Scripts automatizados: Generación automática de lista de archivos y compilación con Webpack

Paso 1: Configuración Inicial del Proyecto

Primero, crea tu proyecto Next.js si aún no lo tienes:

   npx create-next-app@latest pwa-offline
cd pwa-offline
echo "v22.14.0" > .nvmrc

Configura Next.js para exportación estática editando next.config.ts:

   // next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
	output: 'export',
	distDir: 'dist'
}

export default nextConfig

Paso 2: Estructura del Service Worker

Crea la siguiente estructura de directorios:

   mkdir -p src/sw
mkdir scripts

Necesitaremos tres archivos en src/sw/:

   touch src/sw/service-worker.ts
touch src/sw/app-file-list.ts
touch src/sw/version.ts

El flujo de compilación será:

   version.ts (generado)         |
service-worker.ts             |-> webpack -> service-worker.js
app-file-list.ts (generado)   |

Paso 3: Script de Generación Automática

Crea el archivo scripts/generate.js que generará automáticamente la lista de archivos y la versión:

   // scripts/generate.js
const fs = require('fs')
const path = require('path')

// GENERAR VERSION.TS desde package.json
const pkg = require('../package.json')
fs.writeFileSync(
	'./src/sw/version.ts',
	`// This file is auto-generated by scripts/generate.js\nexport const VERSION = '${pkg.version}';\n`
)
console.log(`✓ Generated version.ts with version ${pkg.version}`)

// GENERAR APP-FILE-LIST.TS escaneando carpeta dist
const folderPath = './dist'

function getAllFilesInDir(dir) {
	const entries = fs.readdirSync(dir, { withFileTypes: true })
	return entries.flatMap((entry) => {
		const fullPath = path.join(dir, entry.name)
		return entry.isDirectory() ? getAllFilesInDir(fullPath) : [fullPath]
	})
}

const fileList = getAllFilesInDir(folderPath)
	.map((i) => "'" + i.slice(4) + "'")
	.join(', \n  ')

fs.writeFileSync(
	'./src/sw/app-file-list.ts',
	`// This file is auto-generated by scripts/generate.js
export const APP_FILE_LIST = [
  "/",
  ${fileList}
];
`
)

console.log(`✓ Generated app-file-list.ts with ${getAllFilesInDir(folderPath).length} files`)

Paso 4: Implementar el Service Worker

Crea el Service Worker en src/sw/service-worker.ts:

   // src/sw/service-worker.ts
import { VERSION } from './version'
import { APP_FILE_LIST } from './app-file-list'

const sw: ServiceWorkerGlobalScope = self as unknown as ServiceWorkerGlobalScope

// SW: INSTALL - Cachear archivos al instalar
async function onInstall() {
	console.info('SW : Install : ' + VERSION)
	const cache = await caches.open(VERSION)
	return cache.addAll(APP_FILE_LIST)
}

// SW: ACTIVATE - Limpiar caches antiguas
async function onActivate() {
	console.info('SW : Activate : ' + VERSION)
	const cacheNames = await caches.keys()
	return Promise.all(
		cacheNames
			.filter((cacheName) => cacheName !== VERSION)
			.map((cacheName) => caches.delete(cacheName))
	)
}

// SW: FETCH - Responder desde cache o red (Cache First)
async function onFetch(event: FetchEvent) {
	const cache = await caches.open(VERSION)
	const url = new URL(event.request.url)
	const cacheResource = url.pathname
	const response = await cache.match(cacheResource)
	return response || fetch(event.request)
}

sw.addEventListener('install', (event) => event.waitUntil(onInstall()))
sw.addEventListener('activate', (event) => event.waitUntil(onActivate()))
sw.addEventListener('fetch', (event) => event.respondWith(onFetch(event)))

Crea los archivos iniciales:

   // src/sw/app-file-list.ts
// This file is auto-generated by scripts/generate.js
export const APP_FILE_LIST: string[] = []
   // src/sw/version.ts
// This file is auto-generated by scripts/generate.js
export const VERSION = '0.1.0'

Paso 5: Configuración de TypeScript y Webpack

Crea tsconfig.sw.json para el Service Worker:

   {
	"compilerOptions": {
		"target": "ES2020",
		"module": "ESNext",
		"lib": ["DOM", "webworker", "ES2020"],
		"outDir": "./dist",
		"strict": true,
		"esModuleInterop": true,
		"moduleResolution": "node",
		"skipLibCheck": true
	},
	"include": ["./src/sw/service-worker.ts"]
}

Excluye el directorio src/sw del tsconfig.json principal:

   {
	"exclude": ["node_modules", "src/sw"]
}

Crea webpack.config.js:

   const path = require('path')

module.exports = {
	entry: './src/sw/service-worker.ts',
	module: {
		rules: [
			{
				test: /\.ts$/,
				use: {
					loader: 'ts-loader',
					options: { configFile: 'tsconfig.sw.json' }
				},
				exclude: /node_modules/
			}
		]
	},
	resolve: { extensions: ['.ts', '.js'] },
	output: {
		filename: 'service-worker.js',
		path: path.resolve(__dirname, 'dist')
	}
}

Paso 6: Instalar Dependencias de Build

Instala las herramientas necesarias:

   npm install --save-dev webpack webpack-cli ts-loader

Paso 7: Registrar el Service Worker en Next.js

Crea src/app/template.tsx para registrar el Service Worker:

   // src/app/template.tsx
"use client";

import { useEffect } from "react";

export default function Template({ children }: Readonly<{children: React.ReactNode}>) {
    useEffect(() => {
        // Allow service worker on localhost for development/testing
        // In production, remove this comment and uncomment the check below
        // if (document.domain === "localhost") {
        //     return;
        // }

        if (!('serviceWorker' in navigator)) {
            console.error("Service workers are not supported.");
            return;
        }

        navigator.serviceWorker
            .register("/service-worker.js")
            .then((registration) => {
                console.log("Service worker registration succeeded:", registration);
                if (registration.installing) console.log("SW status: installing");
                if (registration.waiting) console.log("SW status: waiting");
                if (registration.active) console.log("SW status: active");
            })
            .catch((error) => console.error(`Service worker registration failed: ${error}`));
    }, []);

    return <>{children}</>;
}

Paso 8: Scripts de Build en package.json

Actualiza tus scripts en package.json:

   "scripts": {
  "dev": "next dev --turbopack",
  "build": "next build --turbopack",
  "start": "next start",
  "lint": "eslint",
  "preview": "serve dist -p 3001",
  "build:sw:generate": "node scripts/generate.js",
  "build:sw": "webpack",
  "ver:patch": "npm version patch",
  "ver:minor": "npm version minor",
  "ver:major": "npm version major",
  "release:patch": "npm run ver:patch && npm run build && npm run build:sw:generate && npm run build:sw",
  "release:minor": "npm run ver:minor && npm run build && npm run build:sw:generate && npm run build:sw",
  "release:major": "npm run ver:major && npm run build && npm run build:sw:generate && npm run build:sw"
}

Paso 9: Build y Release

Ahora puedes hacer un release completo que:

  1. Incrementa la versión en package.json
  2. Hace build de Next.js
  3. Genera version.ts y app-file-list.ts
  4. Compila service-worker.js
   git add .
git commit -m "Add offline PWA support with Service Worker"
npm run release:patch

¡Verás algo como esto!

   v0.1.1
 Compiled successfully in 1377ms
 Generated version.ts with version 0.1.1
 Generated app-file-list.ts with 38 files
webpack 5.102.0 compiled with 1 warning in 697 ms

Paso 10: Prueba en Local

Instala un servidor estático:

   npm install --save-dev serve

Ejecuta:

   npm run preview

Abre http://localhost:3001 y verifica en DevTools:

1. Application > Service Workers

Deberías ver service-worker.js con estado “activated and is running”:

Service Worker Activo

2. Application > Cache Storage

Deberías ver un cache con nombre 0.1.1 (tu versión) conteniendo todos los archivos:

Cache Storage

3. Network > Offline Mode

Marca el checkbox “Offline” y recarga la página. ¡La aplicación seguirá funcionando! Observa que los recursos se sirven desde el Service Worker:

Red Offline

Mensajes en Consola

Deberías ver:

   SW : Install : 0.1.1
SW : Activate : 0.1.1
Service worker registration succeeded: ...
SW status: active

Resumen de la Implementación

Has aprendido a:

  1. ✅ Configurar Next.js para exportación estática
  2. ✅ Crear un Service Worker con estrategia Cache First
  3. ✅ Generar automáticamente listas de archivos a cachear
  4. ✅ Versionar el caché usando package.json
  5. ✅ Compilar TypeScript a JavaScript con Webpack
  6. ✅ Registrar y probar el Service Worker localmente
  7. ✅ Verificar funcionamiento offline completo

Próximos Pasos Avanzados

Una vez dominado este tutorial básico, puedes explorar:

  • skipWaiting(): Forzar activación inmediata de nuevas versiones
  • postMessage: Comunicación entre SW y página principal
  • controllerchange: Detectar cambios de Service Worker
  • Background Sync: Sincronización diferida de datos
  • Push Notifications: Notificaciones desde el servidor
  • Estrategias híbridas: Network First, Stale While Revalidate

Conclusión

Implementar un Service Worker manualmente te da control total sobre el comportamiento offline de tu aplicación. Aunque existen librerías que simplifican el proceso, entender los fundamentos te permite crear soluciones optimizadas y resolver problemas complejos.

Ahora tienes una PWA completamente funcional sin dependencias externas. ¡Tu aplicación funcionará incluso sin conexión!