Hive Hive
Sign in

fix(server): stop marketing navbar background image re-downloading on every LiveView update

GitHub issue · Closed

Metadata
Source
tuist/tuist #11587
Updated
Jul 5, 2026
Details

What changed

Three related fixes for the marketing site:

  1. phx-update="ignore" on the navbar root (navbar.html.heex) — stops the navbar background image developers-background-<hash>.webp from being re-downloaded on every LiveView update. This is the actual fix for the reported bug.
  2. Dropped the {assigns} spread from all nine marketing LiveViews — restores LiveView change tracking, eliminating a wasteful ~140 KB full-page re-render every 5 s on the stat pages. Complementary efficiency fix.
  3. replace: true on the blog search push_patch (marketing_blog_live.ex) — the per-keystroke search used pushState, stacking one browser-history entry per character; replaceState updates the URL in place. Category/pagination keep pushState (discrete, Back-worthy actions).

The bug

On marketing pages the navbar background image re-downloaded from the network repeatedly, over a healthy WebSocket (no page reload):

  • Stat pages (/build-insights, /test-insights, …): every ~5 s, paced by the “… in the last 24 hours” counter.
  • Blog (/blog): on every keystroke in the search box.

It is easy to miss because the immutable cache hides it — the re-created <img> is normally served from memory. It only surfaces as a network download with DevTools “Disable cache” on (which is how it was reported).

Root cause

The navbar dropdowns are managed entirely by client-side JS hooks (NavbarDropdown / MobileMenuDropdown) that mutate their own DOM (data-open, id, role, aria-*, style.top). On every LiveView DOM patch, morphdom reconciles the navbar subtree against that JS-mutated live DOM, fails to match it, and rebuilds it — inserting a brand-new developers-background.webp <img> (addChild) and forcing a fresh network fetch.

Crucially this happens on every patch regardless of whether the navbar’s own inputs changed — confirmed with a MutationObserver: the <img> is re-created even on a 969-byte diff that doesn’t reference the navbar and with current_path held constant. The image markup is static (sent once, then referenced by id), so it never appears in a diff frame; the re-fetch is a morphdom DOM-reconciliation effect, not a “the image is in the diff” effect.

What drives the patches differs per page:

  • Stat pages: <Layouts.marketing {assigns}> spread the whole assigns map into the layout, defeating change tracking, so the 5 s Tuist.Marketing.Stats PubSub broadcast re-rendered the full page (a captured diff frame was 142,788 bytes, containing every image on the page).
  • Blog: search does a push_patch per keystroke → handle_params → the LiveView patches.

Why this fix (vs. alternatives)

phx-update="ignore" is the framework’s mechanism for “a JS hook owns this DOM subtree; don’t reconcile it” — exactly this situation. I verified the obvious alternatives do not fix the image, because the re-creation is inherent to morphdom reconciling the hook-mutated DOM on every patch:

Attempt Browser history Image re-fetch (cache off)
replace: true (replaceState) ✅ fixed ❌ still re-fetches
path-only current_path ❌ still re-fetches
stable id on the <img> ❌ still re-fetches (parent subtree is rebuilt)
phx-update="ignore" ✅ fixed

Dropping {assigns} alone fixes the stat pages (their updates don’t change the navbar’s inputs, so with tracking restored the navbar isn’t re-rendered) but not the blog (where the navbar legitimately re-renders on search) — which is why phx-update="ignore" is required. Both are kept: phx-update="ignore" for correctness across all pages, {assigns} removal for the ~140 KB-per-5 s efficiency win.

The only way to avoid phx-update="ignore" entirely would be to stop the image being a morphdom-managed <img> — e.g. inlining it as a base64 data-URL background in the CSS bundle — which adds ~79 KB to every marketing page’s CSS and risks a mix-blend-modebackground-blend-mode visual change. Not worth it for a decorative background.

Trade-off

With phx-update="ignore", the navbar’s current_path-dependent bits (language-switcher links, active highlighting) freeze at their mount-time values during a same-LiveView push_patch. Negligible for marketing: cross-page navigation re-mounts the navbar, and the active nav item doesn’t change during blog search.

Impact

  • Navbar background image no longer re-downloads on any marketing page (stat pages, blog, customers, …), on any LiveView update.
  • Stat-page updates go from a ~140 KB full-page diff to a small counter-only diff.
  • Blog search no longer spams browser history — one Back press leaves the page instead of one per character typed.

Validation

Reproduced and verified against a local server with cache disabled (DevTools-equivalent):

  • Before: blog search xxxxx → 6 image downloads (one per keystroke); a MutationObserver showed the <img> re-created on every patch.
  • After: image downloads = 1 (initial load only); browser history.length growth for a 5-char query = 0 (was +5).
  • Stat pages: captured the 142,788-byte full-page diff frame; the network “Initiator” for the webp is conn.onmessage → applyDiff → performPatch → … → addChild (morphdom creating the element inside a diff, not a navigation).
  • Smoke-tested /build-insights with the navbar change: navbar + counter render, no page errors.

Files (11)

  • navbar.html.heexphx-update="ignore" on the navbar root (image fix)
  • marketing_blog_live.exreplace: true on the search patch (history fix)
  • 9 marketing LiveView templates — dropped the {assigns} spread (efficiency): build_insights, test_insights, flaky_tests, selective_testing, cache, blog, blog_post, customers, previews

Follow-ups

  • The {assigns} spread remains in the static marketing_html/* controller pages; inert there (no LiveView diffing), left for a separate consistency sweep if desired.
  • marketing.js sets longPollFallbackMs: 2500 with no guard (unlike app.js), which can cause a reload loop on flaky/high-latency links — unrelated to this bug, flagged for follow-up.

🤖 Generated with Claude Code

Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

Comments

No GitHub comments yet.