Hive
fix(server): stop marketing navbar background image re-downloading on every LiveView update
GitHub issue · Closed
What changed
Three related fixes for the marketing site:
phx-update="ignore"on the navbar root (navbar.html.heex) — stops the navbar background imagedevelopers-background-<hash>.webpfrom being re-downloaded on every LiveView update. This is the actual fix for the reported bug.- 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. replace: trueon the blog searchpush_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 sTuist.Marketing.StatsPubSub 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_patchper 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-mode → background-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.lengthgrowth 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-insightswith the navbar change: navbar + counter render, no page errors.
Files (11)
navbar.html.heex—phx-update="ignore"on the navbar root (image fix)marketing_blog_live.ex—replace: trueon 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 staticmarketing_html/*controller pages; inert there (no LiveView diffing), left for a separate consistency sweep if desired. marketing.jssetslongPollFallbackMs: 2500with no guard (unlikeapp.js), which can cause a reload loop on flaky/high-latency links — unrelated to this bug, flagged for follow-up.
🤖 Generated with Claude Code
Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.
Start a Flight and preserve its objective, outcome, and session here.
No GitHub comments yet.