Published
- 7 min read
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:
- Cierra TODAS las pestañas con tu aplicación (o mejor, cierra el navegador completo)
- Un simple
Ctrl + RNO es suficiente. Usa hard refresh:Ctrl + Shift + R - 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:
- Incrementa la versión en
package.json - Hace build de Next.js
- Genera
version.tsyapp-file-list.ts - 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”:

2. Application > Cache Storage
Deberías ver un cache con nombre 0.1.1 (tu versión) conteniendo todos los archivos:

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:

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:
- ✅ Configurar Next.js para exportación estática
- ✅ Crear un Service Worker con estrategia Cache First
- ✅ Generar automáticamente listas de archivos a cachear
- ✅ Versionar el caché usando
package.json - ✅ Compilar TypeScript a JavaScript con Webpack
- ✅ Registrar y probar el Service Worker localmente
- ✅ 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!