ShipLocal build series · Part 30
Three Classes of Tunnel Bugs
A field guide from one debugging week — hydration, reload loops, observer timing.
This is the field guide we wish we had on day one.
If your app “works on localhost” but misbehaves through a tunnel, don’t start by rewriting the protocol. Start by classifying the failure.
From one week of testing real Next.js apps through ShipLocal, almost every weird symptom fell into three classes.
Deep dives live in Parts 7–10. This post is the skimmable checklist.
Class 1 — HTML is fast, JavaScript is slow
Symptoms
- Navbar / SSR text appear immediately
- Framer Motion heroes animate late
- Page feels “half loaded”
What it usually is
Hydration delay. The tunnel delivered HTML; JS chunks arrived later.
DevTools checks
- Network → filter JS
- Watch
_next/static/chunks/* - Compare HTML timing vs JS timing
Likely causes
- Full response buffering over the control WebSocket
- Base64-in-JSON overhead on every asset
- Many sequential chunks amplifying per-request latency
Read next
- When HTML Is Fast but JavaScript Is Slow (Part 10)
Class 2 — Continuous reload / unstable dev runtime
Symptoms
- Page loads, then reloads forever
- Hot reload never sticks
- DevTools → WS shows failed or flapping sockets
What it usually is
Dev runtime interference — not “the tunnel is flaky.”
Two common roots:
- HTML injection into Next/Vite dev pages (overlay → Fast Refresh → reinject → reload)
- Missing HMR WebSocket relay (
/_next/webpack-hmrnever stays connected)
DevTools checks
- Network → filter WS
- Look for
/_next/webpack-hmr(or Vite client) - View Page Source → search
data-shiplocal-overlay
Likely causes
- Injecting
overlay.jsintowebpack-hmrHTML - HTTP-only proxy (no browser upgrade relay)
- Reinjection without a dedupe marker
Read next
- The Feedback Overlay Reload Loop (Part 8)
- Proxying Webpack HMR Over a Tunnel (Part 9)
Class 3 — Partial UI (headers lag, body shows)
Symptoms
- Section body text appears
motion.header/ titles stay invisible longeruseInViewseems “broken”
What it usually is
A side effect of Class 1 or Class 2 — not a broken IntersectionObserver by itself.
Late hydration, layout shifts from overlay UI, or reload loops all change when observers fire.
Quick test
// temporarily
const isInView = true;
If everything appears instantly, fix the tunnel-side delay first. Treat lagging titles as a side effect of late hydration or unstable reloads — not as a broken useInView by default.
Deep dive on JS/hydration: When HTML Is Fast but JavaScript Is Slow
Suspicion ranking (what we check first)
| Rank | Suspect | Why |
|---|---|---|
| ★★★★★ | HTML injection on dev pages | Explains reload loops cleanly |
| ★★★★★ | Reinjection without dedupe | Creates inject → reload → inject loops |
| ★★★★☆ | Failed HMR WebSocket | Next/Vite depend on it heavily |
| ★★★★☆ | Buffering + base64 overhead | Explains HTML-fast / JS-slow |
| ★★★☆☆ | Compression / content-encoding | Helps bandwidth; can break if headers are wrong |
| ★★☆☆☆ | IntersectionObserver alone | Usually a side effect |
60-second triage
- JS tab — are chunks slow?
- WS tab — is
webpack-hmrconnected? - View source — is overlay injected into dev HTML?
- CSP — would
script-srcblock the overlay? - Run doctor — paste the report
shiplocal doctor
# or
shiplocal doctor --port 3000
Paste the full output into a GitHub issue. That report is more useful than “ShipLocal is slow.”
What to do for client demos today
If you need feedback (💬 overlay), prefer a review-ready preview:
next build && next start
shiplocal 3000
Dev tunnels are great for “does it work?” Review previews are better for “leave structured feedback.”
Guide: How to get client feedback on tunnel previews