nextjs-pwa

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.js PWA Skill

Next.js PWA 开发指南

Quick Reference

快速参考

TaskApproachReference
Add PWA to Next.js appSerwist (recommended)This file → Quick Start
Add PWA without dependenciesManual SWreferences/service-worker-manual.md
Configure cachingSerwist defaultCache or customreferences/caching-strategies.md
Add offline supportApp shell + IndexedDBreferences/offline-data.md
Push notificationsVAPID + web-pushreferences/push-notifications.md
Fix iOS issuesSafari/WebKit workaroundsreferences/ios-quirks.md
Debug SW / LighthouseDevTools + common fixesreferences/troubleshooting.md
Migrate from next-pwaSerwist migrationreferences/serwist-setup.md

任务实现方式参考文档
为Next.js应用添加PWA功能Serwist(推荐)本文档 → 快速开始
无依赖添加PWA功能手动实现Service Workerreferences/service-worker-manual.md
配置缓存策略Serwist默认缓存或自定义配置references/caching-strategies.md
添加离线支持应用壳架构 + IndexedDBreferences/offline-data.md
实现推送通知VAPID + web-pushreferences/push-notifications.md
修复iOS兼容问题Safari/WebKit兼容方案references/ios-quirks.md
调试Service Worker / Lighthouse检测DevTools + 常见问题修复references/troubleshooting.md
从next-pwa迁移Serwist迁移指南references/serwist-setup.md

Quick Start — Serwist (Recommended)

快速开始 — Serwist(推荐方案)

Serwist is the actively maintained successor to next-pwa, built for App Router.
Serwist是next-pwa的官方维护继任者,专为App Router打造。

1. Install

1. 安装依赖

bash
npm install @serwist/next && npm install -D serwist
bash
npm install @serwist/next && npm install -D serwist

2. Create
app/manifest.ts

2. 创建
app/manifest.ts

ts
import type { MetadataRoute } from "next";

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: "My App",
    short_name: "App",
    description: "My Progressive Web App",
    start_url: "/",
    display: "standalone",
    background_color: "#ffffff",
    theme_color: "#000000",
    icons: [
      { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
      { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
    ],
  };
}
ts
import type { MetadataRoute } from "next";

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

3. Create
app/sw.ts
(service worker)

3. 创建
app/sw.ts
(Service Worker文件)

ts
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";

declare global {
  interface WorkerGlobalScope extends SerwistGlobalConfig {
    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
  }
}

declare const self: ServiceWorkerGlobalScope;

const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
  runtimeCaching: defaultCache,
});

serwist.addEventListeners();
ts
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";

declare global {
  interface WorkerGlobalScope extends SerwistGlobalConfig {
    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
  }
}

declare const self: ServiceWorkerGlobalScope;

const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
  runtimeCaching: defaultCache,
});

serwist.addEventListeners();

4. Update
next.config.ts

4. 更新
next.config.ts

ts
import withSerwist from "@serwist/next";

const nextConfig = {
  // your existing config
};

export default withSerwist({
  swSrc: "app/sw.ts",
  swDest: "public/sw.js",
  disable: process.env.NODE_ENV === "development",
})(nextConfig);
That's it — 4 files for a working PWA. Run
next build
and test with Lighthouse.

ts
import withSerwist from "@serwist/next";

const nextConfig = {
  // 现有配置
};

export default withSerwist({
  swSrc: "app/sw.ts",
  swDest: "public/sw.js",
  disable: process.env.NODE_ENV === "development",
})(nextConfig);
完成以上4步即可搭建可运行的PWA。执行
next build
后,使用Lighthouse进行测试。

Quick Start — Manual (No Dependencies)

快速开始 — 手动实现(无依赖)

Use this when you want zero dependencies or are using
output: "export"
.
当你需要零依赖或使用
output: "export"
静态导出时,可采用此方案。

1. Create
app/manifest.ts

1. 创建
app/manifest.ts

Same as above.
与上述步骤相同。

2. Create
public/sw.js

2. 创建
public/sw.js

js
const CACHE_NAME = "app-v1";
const PRECACHE_URLS = ["/", "/offline"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  if (event.request.mode === "navigate") {
    event.respondWith(
      fetch(event.request).catch(() => caches.match("/offline"))
    );
    return;
  }
  event.respondWith(
    caches.match(event.request).then((cached) => cached || fetch(event.request))
  );
});
js
const CACHE_NAME = "app-v1";
const PRECACHE_URLS = ["/", "/offline"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  if (event.request.mode === "navigate") {
    event.respondWith(
      fetch(event.request).catch(() => caches.match("/offline"))
    );
    return;
  }
  event.respondWith(
    caches.match(event.request).then((cached) => cached || fetch(event.request))
  );
});

3. Register SW in layout

3. 在布局中注册Service Worker

tsx
// app/components/ServiceWorkerRegistration.tsx
"use client";

import { useEffect } from "react";

export function ServiceWorkerRegistration() {
  useEffect(() => {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js");
    }
  }, []);
  return null;
}
Add
<ServiceWorkerRegistration />
to your root layout.

tsx
// app/components/ServiceWorkerRegistration.tsx
"use client";

import { useEffect } from "react";

export function ServiceWorkerRegistration() {
  useEffect(() => {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js");
    }
  }, []);
  return null;
}
<ServiceWorkerRegistration />
添加到根布局组件中。

Decision Framework

方案选择框架

ScenarioRecommendation
App Router, wants caching out of the boxSerwist
Static export (
output: "export"
)
Manual SW
Migrating from next-pwaSerwist (drop-in successor)
Need push notificationsEither — see references/push-notifications.md
Need granular cache controlSerwist with custom routes
Zero dependencies requiredManual SW
Minimal PWA (just installable)Manual SW

场景推荐方案
使用App Router,需要开箱即用的缓存功能Serwist
静态导出(
output: "export"
手动实现Service Worker
从next-pwa迁移Serwist(无缝替代方案)
需要推送通知功能两种方案均可 → 参考references/push-notifications.md
需要精细的缓存控制带自定义路由的Serwist方案
要求零依赖手动实现Service Worker
轻量PWA(仅需可安装)手动实现Service Worker

Web App Manifest

Web应用清单

Next.js 13.3+ supports
app/manifest.ts
natively. This generates
/manifest.webmanifest
at build time.
Next.js 13.3+原生支持
app/manifest.ts
,构建时会自动生成
/manifest.webmanifest
文件。

Key fields

核心字段

ts
{
  name: "Full App Name",              // install dialog, splash screen
  short_name: "App",                  // home screen label (≤12 chars)
  description: "What the app does",
  start_url: "/",                     // entry point on launch
  display: "standalone",              // standalone | fullscreen | minimal-ui | browser
  orientation: "portrait",            // optional: lock orientation
  background_color: "#ffffff",        // splash screen background
  theme_color: "#000000",             // browser chrome color
  icons: [
    { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
    { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
    { src: "/icon-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
  ],
  screenshots: [                      // optional: richer install UI
    { src: "/screenshot-wide.png", sizes: "1280x720", type: "image/png", form_factor: "wide" },
    { src: "/screenshot-narrow.png", sizes: "640x1136", type: "image/png", form_factor: "narrow" },
  ],
}
ts
{
  name: "Full App Name",              // 安装弹窗、启动屏显示名称
  short_name: "App",                  // 主屏幕标签(≤12字符)
  description: "What the app does",
  start_url: "/",                     // 启动入口
  display: "standalone",              // 显示模式:standalone | fullscreen | minimal-ui | browser
  orientation: "portrait",            // 可选:锁定屏幕方向
  background_color: "#ffffff",        // 启动屏背景色
  theme_color: "#000000",             // 浏览器导航栏颜色
  icons: [
    { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
    { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
    { src: "/icon-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
  ],
  screenshots: [                      // 可选:丰富安装界面
    { src: "/screenshot-wide.png", sizes: "1280x720", type: "image/png", form_factor: "wide" },
    { src: "/screenshot-narrow.png", sizes: "640x1136", type: "image/png", form_factor: "narrow" },
  ],
}

Manifest tips

清单配置技巧

  • Always include both 192x192 and 512x512 icons (Lighthouse requirement)
  • Add a
    maskable
    icon for Android adaptive icons
  • screenshots
    enable the richer install sheet on Android/desktop Chrome
  • theme_color
    should match your
    <meta name="theme-color">
    in layout

  • 必须同时包含192x192和512x512尺寸的图标(Lighthouse检测要求)
  • 添加
    maskable
    类型图标以支持Android自适应图标
  • screenshots
    字段可在Android/桌面版Chrome中启用更丰富的安装界面
  • theme_color
    应与布局中的
    <meta name="theme-color">
    保持一致

Service Worker Essentials

Service Worker 核心知识

Lifecycle

生命周期

  1. Install — SW downloaded,
    install
    event fires, precache assets
  2. Waiting — New SW waits for all tabs to close (unless
    skipWaiting
    )
  3. Activate — Old caches cleaned up, SW takes control
  4. Fetch — SW intercepts network requests
  1. 安装 — 下载Service Worker,触发
    install
    事件,预缓存资源
  2. 等待 — 新的Service Worker等待所有标签页关闭(除非设置
    skipWaiting
  3. 激活 — 清理旧缓存,Service Worker接管控制权
  4. 请求拦截 — Service Worker拦截网络请求

Update flow

更新流程

When a new SW is detected:
  • skipWaiting: true
    — immediately activates (may break in-flight requests)
  • Without
    skipWaiting
    — waits for all tabs to close, then activates
  • Notify users of updates with
    workbox-window
    or manual
    controllerchange
    listener
检测到新的Service Worker时:
  • skipWaiting: true
    — 立即激活(可能中断正在进行的请求)
  • 未设置
    skipWaiting
    — 等待所有标签页关闭后激活
  • 可通过
    workbox-window
    或手动监听
    controllerchange
    事件通知用户更新

Registration scope

注册作用域

  • SW at
    /sw.js
    controls all pages under
    /
  • SW at
    /app/sw.js
    only controls
    /app/*
  • Always place SW at root unless you have a specific reason not to

  • 位于
    /sw.js
    的Service Worker控制
    /
    下的所有页面
  • 位于
    /app/sw.js
    的Service Worker仅控制
    /app/*
    路径下的页面
  • 除非有特殊需求,否则应将Service Worker放在根目录

Caching Strategies Quick Reference

缓存策略快速参考

StrategyUse ForSerwist Class
Cache FirstStatic assets, fonts, images
CacheFirst
Network FirstAPI data, HTML pages
NetworkFirst
Stale While RevalidateSemi-static content (CSS/JS)
StaleWhileRevalidate
Network OnlyAuth endpoints, real-time data
NetworkOnly
Cache OnlyPrecached content only
CacheOnly
Serwist's
defaultCache
provides sensible defaults. For custom strategies, see references/caching-strategies.md.

策略适用场景Serwist对应类
缓存优先静态资源、字体、图片
CacheFirst
网络优先API数据、HTML页面
NetworkFirst
缓存回源更新半静态内容(CSS/JS)
StaleWhileRevalidate
仅网络授权接口、实时数据
NetworkOnly
仅缓存仅预缓存内容
CacheOnly
Serwist的
defaultCache
提供了合理的默认配置。如需自定义策略,请参考references/caching-strategies.md。

Offline Support Basics

离线支持基础

App shell pattern

应用壳架构

Precache the app shell (layout, styles, scripts) so the UI loads instantly offline. Dynamic content loads from cache or shows a fallback.
预缓存应用壳(布局、样式、脚本),确保UI可立即离线加载。动态内容从缓存读取或显示降级页面。

Online/offline detection hook

在线/离线状态检测钩子

tsx
"use client";
import { useSyncExternalStore } from "react";

function subscribe(callback: () => void) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true // SSR: assume online
  );
}
tsx
"use client";
import { useSyncExternalStore } from "react";

function subscribe(callback: () => void) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true // SSR环境默认假设在线
  );
}

Offline fallback page

离线降级页面

Create
app/offline/page.tsx
and precache
/offline
in your SW. When navigation fails, serve this page.
For IndexedDB, background sync, and advanced offline patterns, see references/offline-data.md.

创建
app/offline/page.tsx
,并在Service Worker中预缓存
/offline
路径。当导航失败时,返回该页面。
如需IndexedDB、后台同步等高级离线方案,请参考references/offline-data.md。

Install Prompt Handling

安装提示处理

beforeinstallprompt
(Chrome/Edge/Android)

beforeinstallprompt
事件(Chrome/Edge/Android)

tsx
"use client";
import { useState, useEffect } from "react";

export function InstallPrompt() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null);

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e);
    };
    window.addEventListener("beforeinstallprompt", handler);
    return () => window.removeEventListener("beforeinstallprompt", handler);
  }, []);

  if (!deferredPrompt) return null;

  return (
    <button
      onClick={async () => {
        deferredPrompt.prompt();
        const { outcome } = await deferredPrompt.userChoice;
        if (outcome === "accepted") setDeferredPrompt(null);
      }}
    >
      Install App
    </button>
  );
}
tsx
"use client";
import { useState, useEffect } from "react";

export function InstallPrompt() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null);

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e);
    };
    window.addEventListener("beforeinstallprompt", handler);
    return () => window.removeEventListener("beforeinstallprompt", handler);
  }, []);

  if (!deferredPrompt) return null;

  return (
    <button
      onClick={async () => {
        deferredPrompt.prompt();
        const { outcome } = await deferredPrompt.userChoice;
        if (outcome === "accepted") setDeferredPrompt(null);
      }}
    >
      安装应用
    </button>
  );
}

iOS detection

iOS设备检测

iOS doesn't fire
beforeinstallprompt
. Detect iOS and show manual instructions:
ts
function isIOS() {
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
}

function isStandalone() {
  return window.matchMedia("(display-mode: standalone)").matches
    || (navigator as any).standalone === true;
}
Show a banner: "Tap Share then Add to Home Screen" for iOS Safari users.

iOS不会触发
beforeinstallprompt
事件。需检测iOS设备并显示手动安装指引:
ts
function isIOS() {
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
}

function isStandalone() {
  return window.matchMedia("(display-mode: standalone)").matches
    || (navigator as any).standalone === true;
}
显示提示横幅:"点击分享按钮,选择添加到主屏幕"(针对iOS Safari用户)。

Push Notifications Quick Start

推送通知快速开始

1. Generate VAPID keys

1. 生成VAPID密钥

bash
npx web-push generate-vapid-keys
bash
npx web-push generate-vapid-keys

2. Subscribe in client

2. 客户端订阅

ts
async function subscribeToPush() {
  const reg = await navigator.serviceWorker.ready;
  const sub = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
  });
  await fetch("/api/push/subscribe", {
    method: "POST",
    body: JSON.stringify(sub),
  });
}
ts
async function subscribeToPush() {
  const reg = await navigator.serviceWorker.ready;
  const sub = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
  });
  await fetch("/api/push/subscribe", {
    method: "POST",
    body: JSON.stringify(sub),
  });
}

3. Handle in SW

3. Service Worker中处理

ts
self.addEventListener("push", (event) => {
  const data = event.data?.json() ?? { title: "Notification" };
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: "/icon-192.png",
    })
  );
});
For server-side sending, VAPID setup, and full implementation, see references/push-notifications.md.

ts
self.addEventListener("push", (event) => {
  const data = event.data?.json() ?? { title: "Notification" };
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: "/icon-192.png",
    })
  );
});
如需服务端发送、VAPID配置及完整实现,请参考references/push-notifications.md。

Troubleshooting Cheat Sheet

问题排查速查表

ProblemFix
SW not updatingAdd
skipWaiting: true
or hard refresh (Shift+Cmd+R)
App not installableCheck manifest: needs
name
,
icons
,
start_url
,
display
Stale content after deployBump cache version or use content-hashed URLs
SW registered in devDisable in dev:
disable: process.env.NODE_ENV === "development"
iOS not showing installiOS has no install prompt — show manual instructions
Lighthouse PWA failsCheck HTTPS, valid manifest, registered SW, offline page
Next.js rewrite conflictsEnsure SW is served from
/sw.js
, not rewritten
For detailed debugging steps, see references/troubleshooting.md.

问题解决方法
Service Worker未更新添加
skipWaiting: true
或强制刷新(Shift+Cmd+R)
应用无法安装检查清单:需包含
name
icons
start_url
display
字段
部署后内容未更新升级缓存版本或使用带内容哈希的URL
开发环境中注册了Service Worker在开发环境禁用:
disable: process.env.NODE_ENV === "development"
iOS未显示安装提示iOS无自动安装提示 — 显示手动指引
Lighthouse PWA检测失败检查HTTPS、有效清单、已注册的Service Worker、离线页面
Next.js路由重写冲突确保Service Worker从
/sw.js
提供服务,未被重写
如需详细调试步骤,请参考references/troubleshooting.md。

Assets & Templates

资源与模板

  • assets/manifest-template.ts
    — Complete app/manifest.ts with all fields
  • assets/sw-serwist-template.ts
    — Serwist SW with custom routes and offline fallback
  • assets/sw-manual-template.js
    — Manual SW with all strategies
  • assets/next-config-serwist.ts
    — next.config.ts with withSerwist
  • assets/manifest-template.ts
    — 完整的app/manifest.ts模板(包含所有字段)
  • assets/sw-serwist-template.ts
    — 带自定义路由和离线降级的Serwist Service Worker模板
  • assets/sw-manual-template.js
    — 包含所有策略的手动实现Service Worker模板
  • assets/next-config-serwist.ts
    — 集成withSerwist的next.config.ts模板

Generator Script

生成脚本

bash
python scripts/generate_pwa_config.py <project-name> --approach serwist|manual [--push] [--offline]
Scaffolds PWA files based on chosen approach and features.
bash
python scripts/generate_pwa_config.py <project-name> --approach serwist|manual [--push] [--offline]
根据选择的实现方式和功能,自动生成PWA相关文件。