Next.js 16 App Router PWA + Supabase SSR:移动端/PWA无限加载问题(疑似手动Service Worker缓存RSC/Flight请求导致)
Based on your setup (Next.js 16 App Router, Supabase SSR auth, manual Service Worker), here's a targeted breakdown of the issues and actionable fixes:
1. Recommended Manual Service Worker Strategy for Authenticated Next.js 16 Apps
Prioritize NetworkOnly for dynamic/authenticated routes and internal Next.js requests, while caching static assets aggressively. The core rules should be:
- Navigation requests: NetworkFirst (with offline fallback to your
/offlinepage) - Next.js internal RSC/Flight/data requests: NetworkOnly (never cache)
- Static assets (
/_next/static/*, images, icons): CacheFirst/StaleWhileRevalidate - Authenticated API/Supabase calls: NetworkOnly (to avoid stale session data)
2. Should App Router Internal Requests Use NetworkOnly?
Absolutely. These requests carry dynamic, user-specific data (like RSC payloads or session-dependent UI) and are tied to Next.js's internal routing state. Caching them will lead to mismatched UI, infinite loading, or stale authenticated content—exactly the issues you're seeing on mobile/PWA.
3. How to Detect Next.js Internal Requests in Service Worker
You can reliably identify these requests using a combination of headers and URL patterns:
- Check for
Accept: text/x-component(RSC payloads) - Look for request headers like
Next-Router-State-TreeorRSC - Match URLs starting with
/_next/data/(App Router data fetching endpoints)
Here's a reusable helper function:
function isNextInternalRequest(request) { const url = new URL(request.url); return ( request.headers.get('Accept')?.includes('text/x-component') || request.headers.has('Next-Router-State-Tree') || request.headers.has('RSC') || url.pathname.startsWith('/_next/data/') ); }
4. Is Caching Non-Static Resources Safe?
Only if they're static, immutable, and non-user-specific. Avoid caching:
- Authenticated routes/pages
- API calls (especially Supabase auth/session endpoints)
- Any request that returns user-specific data
- Next.js internal requests (as noted above)
Caching these will almost certainly cause stale data or authentication mismatches.
5. Should You Remove NetworkFirst+Cache for Non-Static Requests?
Yes, for non-static, dynamic, or authenticated requests. Replace it with NetworkOnly to ensure fresh data on every load. You can keep NetworkFirst only for public, unauthenticated navigation requests (with offline fallback), but even that should be used cautiously if your public content changes frequently.
6. Known Interactions Between Supabase SSR Middleware and PWA/SW?
The main risk is stale cookie/session data being cached by the SW. If your SW caches authenticated requests, it might serve stale session cookies or invalidated session data, leading to redirect loops (302s to login) or infinite loading. Your current middleware looks correct, but ensuring the SW doesn't cache requests that carry session cookies (like authenticated routes) is critical.
Modified Service Worker Code (Fixed Caching Logic)
Here's the updated sw.js that implements the correct strategies and avoids caching problematic requests:
/** * Pharmagoli Loyalty — Service Worker * * Handles caching strategies, offline fallback, and push notifications. * This is a manual SW that replaces the broken next-pwa generated one * (next-pwa does not work with Turbopack / Next.js 16). * * Updated Strategies: * - Navigation requests → NetworkFirst (fresh pages, offline fallback) * - Next.js internal RSC/Flight/data requests → NetworkOnly (never cache) * - Supabase / API calls → NetworkOnly (never cache) * - _next/static/* → CacheFirst (immutable, hashed filenames) * - Images / icons → StaleWhileRevalidate * - Authenticated routes → NetworkOnly (no cache) */ const CACHE_VERSION = 'v5' const PAGES_CACHE = 'pharmagoli-pages-' + CACHE_VERSION const STATIC_CACHE = 'pharmagoli-static-' + CACHE_VERSION const ASSETS_CACHE = 'pharmagoli-assets-' + CACHE_VERSION const EXPECTED_CACHES = [PAGES_CACHE, STATIC_CACHE, ASSETS_CACHE] // Helper to detect Next.js internal RSC/Flight/data requests function isNextInternalRequest(request) { const url = new URL(request.url); return ( request.headers.get('Accept')?.includes('text/x-component') || request.headers.has('Next-Router-State-Tree') || request.headers.has('RSC') || url.pathname.startsWith('/_next/data/') ); } // Helper to detect Supabase/API requests function isApiOrSupabaseRequest(request) { const url = new URL(request.url); return url.pathname.startsWith('/api/') || url.host.includes('supabase.co'); } // Helper to detect authenticated routes (matches your middleware's public routes) function isAuthenticatedRoute(request) { const url = new URL(request.url); const publicRoutes = ['/auth', '/api/auth', '/offline']; return !publicRoutes.some(route => url.pathname.startsWith(route)); } // ── Install ───────────────────────────────────────────────────────────────── self.addEventListener('install', function (event) { self.skipWaiting(); // Pre-cache the offline fallback page event.waitUntil( caches.open(PAGES_CACHE).then(function (cache) { return cache.add('/offline'); }) ); }); // ── Activate ──────────────────────────────────────────────────────────────── self.addEventListener('activate', function (event) { event.waitUntil( Promise.all([ self.clients.claim(), // Clean up old, outdated caches caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (!EXPECTED_CACHES.includes(cacheName)) { return caches.delete(cacheName); } }) ); }) ]) ); }); // ── Fetch ─────────────────────────────────────────────────────────────────── self.addEventListener('fetch', function (event) { const request = event.request; const url = new URL(request.url); // Skip non-GET requests (e.g., POST/PUT for login) if (request.method !== 'GET') return; // 1. Next.js internal requests → NetworkOnly (never cache) if (isNextInternalRequest(request)) { event.respondWith(fetch(request)); return; } // 2. API/Supabase requests → NetworkOnly (never cache) if (isApiOrSupabaseRequest(request)) { event.respondWith(fetch(request)); return; } // 3. Authenticated routes → NetworkFirst with offline fallback if (isAuthenticatedRoute(request) && request.mode === 'navigate') { event.respondWith( fetch(request).catch(() => caches.match('/offline')) ); return; } // 4. Static Next.js assets → CacheFirst (immutable, hashed filenames) if (url.pathname.startsWith('/_next/static/')) { event.respondWith( caches.match(request).then(cachedResponse => { if (cachedResponse) return cachedResponse; return fetch(request).then(networkResponse => { caches.open(STATIC_CACHE).then(cache => { cache.put(request, networkResponse.clone()); }); return networkResponse; }); }) ); return; } // 5. Images/icons → StaleWhileRevalidate (serve cached, update in background) if (url.pathname.match(/\.(png|jpg|jpeg|svg|ico)$/)) { event.respondWith( caches.match(request).then(cachedResponse => { const fetchPromise = fetch(request).then(networkResponse => { caches.open(ASSETS_CACHE).then(cache => { cache.put(request, networkResponse.clone()); }); return networkResponse; }); return cachedResponse || fetchPromise; }) ); return; } // 6. Public navigation requests → NetworkFirst with offline fallback if (request.mode === 'navigate') { event.respondWith( fetch(request).catch(() => caches.match('/offline')) ); return; } // Fallback: NetworkOnly for all other requests event.respondWith(fetch(request)); });
Additional Tips
- Force SW Activation: Use a new cache version (like
v5) to ensure users get the updated Service Worker immediately - Test Mobile Conditions: Use Chrome DevTools' "Throttling" to simulate slow mobile networks and offline states
- Monitor Requests: Check mobile dev tools for cached responses to internal Next.js requests to confirm the SW is working correctly
内容的提问来源于stack exchange,提问作者Alessandro Goli

