A multi-app Next.js storefront lets shoppers browse categories, open product pages, and check out while the App Router prefetches the next route in the background. When the network wobbles on a phone in a tunnel, I want that tab to pause gracefully and resume without a hard reload. Next.js 16's experimental useOffline config is built for that deployed tab. I had it sitting in a shared base next.config that every app imports, including the pnpm dev session I leave open on localhost all afternoon.
useOffline is the config switch that turns on framework offline detection. Once it is on at build time, the client loads an offline module that listens for browser offline events and failed RSC fetches. When the app thinks it is offline, a HEAD probe / poll loop fires: a HEAD request to the current URL with the RSC header, then another attempt after a backoff delay (500 ms, then 1 s, 2 s, up to 3 s). That loop is meant for preview and production, where Instant Nav and partial prefetch should survive a blip. On my laptop the same lines showed up in the dev server log, over and over, while I was not testing offline at all. The shared config never asked which environment I was in.
What showed up in the terminal
The first time I noticed it, I assumed HMR or a runaway prefetch. The pattern was steadier: HEAD / with the RSC header, then ordinary RSC traffic, then the HEAD again on the backoff schedule from the Next.js source. That matches the connectivity check in the offline module, not a random bug. I was the developer staring at the terminal beside an idle localhost tab, not a shopper on a flaky LTE link. The probes were doing their job for a feature I had accidentally enabled everywhere.
Instant Nav resilience is worth keeping on Vercel preview and production. Local pnpm dev does not need the same retry queue. The trap was not "offline support is bad." It was storing a deploy-only behavior as a constant in shared config instead of deriving it when next.config is evaluated.
Switch to Local dev, flip Shared config wiring to Constant true, then press Simulate connectivity drop and watch the request log fill with HEAD lines. That is the noise I wanted out of my daily terminal.
Simulation
useOffline config vs runtime
Shared Next config can bake in a deploy-only flag. The demo fakes the client-side HEAD poll loop from Next.js 16 offline support.
Offline detection is armed. Simulate a drop to see HEAD probes with backoff (500 ms, then 1 s, 2 s, 3 s cap).
Request log (newest first)
- No probes yet.
Derive the flag at config time
Shared config is still the right place for one experimental block across apps. The missing piece is an environment-shaped boolean, not a second config file per app. On Vercel, VERCEL_ENV is preview or production on deployed builds and unset or development locally. I read that (or NODE_ENV when I am not on Vercel) when the config module loads:
export function shouldUseOffline(vercelEnv: string | undefined = process.env.VERCEL_ENV): boolean {
return vercelEnv === 'preview' || vercelEnv === 'production'
}const nextConfig: NextConfig = {
experimental: {
useOffline: shouldUseOffline(),
},
}Preview and production keep offline detection, the useOffline() hook from next/offline, and the HEAD poll loop when a tab actually goes offline. Local dev builds with the flag off, so the offline module never ships and my terminal stays readable. If I need to test the hook locally, I opt in with an explicit env var rather than silently turning it on for every developer.
When this pattern applies
Use an env-derived gate when a shared Next config carries experiments that only make sense on a deployed origin: offline retry, skew cookies, production-only tracing, anything that changes client networking. Skip it for flags that are genuinely safe everywhere (package import optimization, scroll restoration). When in doubt, ask whether a constant true would annoy someone running next dev for eight hours.
The reusable rule I keep: deploy behaviors belong in functions of environment, not in constants that every import inherits. The next experimental flag in the shared base gets the same question before it lands.
Happy coding!
Sander