ShipLocal
← Back to blog

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

  1. Network → filter JS
  2. Watch _next/static/chunks/*
  3. 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


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:

  1. HTML injection into Next/Vite dev pages (overlay → Fast Refresh → reinject → reload)
  2. Missing HMR WebSocket relay (/_next/webpack-hmr never stays connected)

DevTools checks

  1. Network → filter WS
  2. Look for /_next/webpack-hmr (or Vite client)
  3. View Page Source → search data-shiplocal-overlay

Likely causes

  • Injecting overlay.js into webpack-hmr HTML
  • HTTP-only proxy (no browser upgrade relay)
  • Reinjection without a dedupe marker

Read next


Class 3 — Partial UI (headers lag, body shows)

Symptoms

  • Section body text appears
  • motion.header / titles stay invisible longer
  • useInView seems “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)

RankSuspectWhy
★★★★★HTML injection on dev pagesExplains reload loops cleanly
★★★★★Reinjection without dedupeCreates inject → reload → inject loops
★★★★☆Failed HMR WebSocketNext/Vite depend on it heavily
★★★★☆Buffering + base64 overheadExplains HTML-fast / JS-slow
★★★☆☆Compression / content-encodingHelps bandwidth; can break if headers are wrong
★★☆☆☆IntersectionObserver aloneUsually a side effect

60-second triage

  1. JS tab — are chunks slow?
  2. WS tab — is webpack-hmr connected?
  3. View source — is overlay injected into dev HTML?
  4. CSP — would script-src block the overlay?
  5. 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


Related posts