You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 16 App Router PWA + Supabase SSR:移动端/PWA无限加载问题(疑似手动Service Worker缓存RSC/Flight请求导致)

Fixing Mobile/PWA Infinite Loading Issues with Next.js 16 App Router + Supabase + Manual Service Worker

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:


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 /offline page)
  • 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-Tree or RSC
  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 10:33:10