Developer debugging Shopify web pixel events after Liquid partial soft navigation on a storefront

Shopify Soft Navigation and the Web Pixel Gap: The Front Door Clicker

Shopify Soft Navigation and the Web Pixel Gap: The Front Door Clicker

Aug 1, 2026 Updated Oct 26, 2026

Octavian Contis 15 minutes

Share

Soft navigation made our test store feel like an app, and the ad pixels stopped counting pages.

Shopify's Liquid partials preview lets a theme name server-rendered regions and swap them without reloading the document. We forked Horizon 4.1.3 on a preview store and built it with @shopify/partial-rendering, so the header, footer, and cart chrome stay in the DOM while only the main content region changes when a buyer clicks.

The browse path felt close to instant. Marketing would see something else: Meta Events Manager and GA4 route counts drift from server reality, because shopify:page:view fires in the standard-events inspector while app pixels in their sandboxes often never receive page_viewed for those in-theme routes.

This work depends on Liquid July '26, StorefrontRenderer, and the agentic editor preview. It is experimental, and the pixel bridge below should never be a production default.

Introduction

This article maps the measurement job that appears after you ship a persistent shell: why web pixels go quiet on a region swap, what we tried before touching platform internals, and how to plan attribution until Shopify owns that path. The layout job of naming a page-content region and swapping main content without reloading header and footer lives in Shopify Liquid Partials: Swap Main Content Without Reloading Header and Footer.

Picture a shop with a clicker on the front door. Every time someone walks in, it adds one, and the owner reads that number as footfall. Once a customer is inside, they can wander from aisle to aisle all afternoon and the clicker never moves. I call this the front door clicker. Web pixels count the front door, which is the full document load, while soft navigation keeps the buyer walking between aisles inside a document that never reloads.

On our password-protected liquid-july preview store (password: ocontis) we measured repeat navigation roughly 24× faster than full reloads, with a modest first-load cost from the extra runtime. That visible win aligns with the persistent shell we describe in high-performance theme work. The harder question arrives when marketing opens Events Manager and route-level counts stop matching server-side reality.

The fork is open source on GitHub. We also posted a shorter experiment summary for other theme builders comparing notes. If your stack already struggles with script churn in global chrome, read common theme mistakes that kill conversion before adding interceptors. Partials change where HTML lands; they do not reduce the cost of twelve competing analytics snippets in the header.

Why do the pixels go quiet?

On a full page load, content_for_header injects the inline Trekkie bootstrap and loads Web Pixels Manager (WPM). That sets ShopifyAnalytics.meta, calls ShopifyAnalytics.lib.page() and lib.track(), and initialises WPM once, so pixel iframes subscribe inside WPM and receive page_viewed on first paint. The theme also dispatches shopify:page:view from page-view-event.js.

After a partial swap, the URL and #MainContent update and WPM stays as it was. Platform scripts from content_for_header initialise once per document. Soft navigation then replaces only the content partial and runs a post-apply lifecycle. Analytics split across two buses; only one of them updates automatically on a region swap.

Mermaid diagram

Experimental theme bridgeTwo event busesPlatform (once per document)Soft navigationTheme layoutgap: no supported APIPersistent shellheader · footer · cartpage-content partialswapped on soft navPrefetch on hover / focus /viewportIn-scope link clickpartials.fetch + applyView Transition (applyonly)afterTransition lifecyclehead sync · scripts · events· menu drawercontent_for_headerTrekkie · WPM · pixelsshopify:page:viewstandard storefront eventspage_viewed ·collection_viewedweb pixel customer eventsWPM init capturebefore content_for_headerpartial-pixel-events.jsTrekkie replay + WPMpublish

The dashed path is the part we dislike: intercepting Web Pixels Manager before Shopify freezes the global, then manually publishing customer events after each navigation.

How does the soft navigation work?

A custom element, <partial-navigation>, listens for clicks on in-scope links (data-partial-nav or anything inside a scoped header or footer container). It prevents the default navigation, pushes the URL with the History API, and fetches fresh server HTML for the page-content partial alongside any missing template stylesheets.

The theme warms that fetch before the click. Header links prefetch on connect, product cards prefetch as they enter the viewport, and debounced hover, pointerdown, and keyboard focus do the same, with results held in a short-lived cache of 24 entries for 30 seconds.

partials.apply() swaps the region while keeping focus, form state, and scroll where possible. An optional View Transition animates [data-partial-page-root] inside #MainContent rather than the whole <main> element, so the header shell stays out of the snapshot. popstate runs the same flow for back and forward.

Timing matters here. The View Transition snapshot only captures DOM state during partials.apply(), so head sync, scroll reset, script re-execution, and analytics replay run in an afterTransition callback that keeps morph and slide transitions smooth.

After every apply, the theme syncs theme-owned head metadata marked data-partial-head-managed, re-executes section scripts, fires shopify:section:load, aligns cart badge counts, and dispatches standard page view events. Cart mutations are separate: Shopify.actions emits shopify:cart:lines-update, and the theme refreshes only the nested header-cart partial.

Rapid clicks abort stale fetches through AbortController, and duplicate navigations to the same URL share one in-flight promise. A failed fetch falls back to a full page load so the buyer never sits on stale content.

All of this follows the partials model Shopify is previewing and the Declarative Partial Updates thinking on the web platform: named regions, server HTML, and client-side replacement with Liquid still the source of truth.

What breaks when the header never reloads?

Keeping the header mounted is the point, and it creates work a full reload used to handle for free. Liquid renders aria-current="page" on first paint, so after a soft nav the theme walks the header links and updates aria-current from window.location (syncHeaderNavActiveState() in partial-page-lifecycle.js).

The mobile drawer is the other trap. A nav link inside an open drawer navigates correctly, but the drawer never unmounts, so without an explicit close after partials.apply() buyers land on the new page with the menu still covering the screen. The lifecycle calls header-drawer.close() using the same animation path as the close button.

Header height CSS variables are measured once at load and on resize. Re-measuring after each swap caused hero layout jumps when page transitions were on.

Is it actually faster?

We measured on 1 August 2026 against the horizon-partials repo on liquid-july.myshopify.com, using shopify theme dev locally. The baseline was commit cdf8796, Horizon 4.1.3 with full-page navigation, and the partials build was commit c058b73, the same theme with <partial-navigation>, prefetch, and view transitions.

Navigation timing runs from click until #MainContent shows the new product page. Across five runs per scenario, the full reload took a median of 1,187 ms. The partial swap took a median of 49 ms, roughly 24 times faster.

LabelCommitDescription
Baselinecdf8796Horizon 4.1.3 with standard full-page navigation
Partialsc058b73Same theme + Liquid partials, <partial-navigation>, prefetch, view transitions

Summary. Headline numbers from five runs per scenario (median reported):

MetricBaseline (full reload)Partials (soft nav)Change
Route change: time to new content1,187 ms49 ms~24× faster (−96%)
Route change: DOM ready (baseline only)1,144 msn/an/a
First visit: Largest Contentful Paint7,577 ms8,891 ms+1,315 ms
First visit: First Contentful Paint6,227 ms6,716 ms+489 ms
First visit: Total Blocking Time29 ms114 ms+85 ms
First visit: Cumulative Layout Shift0.000060.00006No change
First visit: Lighthouse performance score6158−3

Partial updates trade a small first-load cost for dramatically faster in-store browsing. After the shell mounts, collection to product feels near-instant instead of waiting about 1.2 seconds per full reload.

Median time until new product page content is visible in #MainContent

The five baseline runs were 1,249 ms and 1,187 ms, then 1,050 ms, 1,120 ms, and 1,214 ms. The partials runs were 55 ms and 49 ms, then 46 ms, 50 ms, and 45 ms.

RunBaselinePartials
11,249 ms55 ms
21,187 ms49 ms
31,050 ms46 ms
41,120 ms50 ms
51,214 ms45 ms
Median1,187 ms49 ms

That is about 1.1 seconds saved per click, with no white flash and no chrome re-render. On a full reload the browser re-fetches and re-parses the whole document, and median DOMContentLoaded on the product page was about 1,144 ms. These are cold-click numbers, so prefetch can make later clicks feel faster still.

First visit is the cost side, measured with Lighthouse on /collections/all under mobile throttling with an empty cache. The performance score fell from 61 to 58.

Core Web Vital / metricBaselinePartialsNotes
Performance score6158Slight regression from partial-navigation modules
First Contentful Paint (FCP)6.2 s6.7 s+0.5 s
Largest Contentful Paint (LCP)7.6 s8.9 s+1.3 s
Total Blocking Time (TBT)29 ms114 ms+85 ms main-thread blocking
Cumulative Layout Shift (CLS)0.000060.00006Identical: no layout instability added
Speed Index6.2 s6.7 sTracks FCP closely on this page

Largest Contentful Paint rose from 7,577 ms to 8,891 ms, an increase of 1,315 ms. First Contentful Paint went from 6,227 ms to 6,716 ms, which is 489 ms slower, and Speed Index tracked it from 6.2 s to 6.7 s. Total Blocking Time climbed from 29 ms to 114 ms. Cumulative Layout Shift stayed at 0.00006 in both builds, so the shell does not shift when partials are on.

The extra cost comes from partial-navigation.js, lifecycle sync, view transitions, and the pixel bridge, which all run once on cold load. Those absolute seconds are dominated by local dev-server latency and theme asset volume, so treat them as directional and trust the relative comparison taken minutes apart on the same machine and store.

We left out transfer sizes, bundle analysis, Theme Editor (partials are disabled there by design), production CDN caching, and prefetch hit rate. The baseline had no <partial-navigation> element and zero partial-nav links, while the partials build had 40.

Detection flagBaselinePartials
<partial-navigation> presentNoYes
Partial-nav links040
Navigation modeFull documentPartial region swap

Browse-heavy stores where shoppers open several products per session gain the most. If SEO landing pages are the main entry and first-visit LCP is the constraint, defer partial modules on templates that never soft-navigate, and re-run on an HTTPS production preview with CDN caching before treating Core Web Vitals as launch criteria.

Two buses, two sets of listeners

Shopify runs two event pipes, and only one of them follows a region swap automatically.

LayerExamplesListeners
Standard storefront eventsshopify:page:view, shopify:collection:viewApps, agents, standard-events inspector
Web pixel customer eventspage_viewed, collection_viewed, product_viewedAll web pixels: custom and app (Meta, Google, Klaviyo, session replay, etc.)

We first assumed dispatching shopify:page:view after the swap would cover downstream tracking, since it is documented and exactly what themes should emit when the active page changes. It reached the inspector overlay and stopped there.

Headless browser probes confirmed the split. After a soft nav, ShopifyAnalytics.meta.page.pageType updated correctly and Trekkie shim pushes fired, yet the pixel sandbox showed no page_viewed the way it does on a full load. When you debug pixels, the sandbox logs sit in the pixel iframe console, so switch DevTools away from the top-level tab.

On partial navigation, Trekkie's shim queue may still accept late page and track calls, yet that path feeds Monorail parity telemetry rather than the WPM publish() call that delivers to pixels.

What we tried before touching internals

Our first attempt was Trekkie replay from fetched HTML. After each navigation we parsed the destination's inline scripts for var meta = …, ShopifyAnalytics.lib.page(…), and lib.track(…), merged the meta, and replayed the calls, which updated analytics state and Monorail events while pixel sandboxes stayed silent.

Next we tried Shopify.analytics.publish('page_viewed', …). On the storefront that function is wired to publishCustomEvent only, standard event names return false, and public docs describe no theme API for standard pixel events on soft nav.

Re-running content_for_header was the third idea, and we dropped it. Platform head scripts assume one run per document, so a second run risks double pixel registration and fights with non-configurable globals, which is why head sync only touches theme-owned nodes.

Where the pixel counter gets thrown away

Reading the minified WPM bundle and the inline wpmLoader in content_for_header showed the detail. On init, WPM returns an API with publish for standard customer events and publishCustomEvent, and the loader keeps only the second:

var api = window.webPixelsManager.init(config);
Shopify.analytics.publish = api.publishCustomEvent; // api.publish discarded

The publish that pixels need is discarded. window.webPixelsManager is defined with configurable: false, so it cannot be replaced afterwards.

Trekkie maps page_rendered to page_viewed for Shopify.evids, but post-init lib.page() goes through a shim handler that produces Monorail payloads instead of O.publish('page_viewed', …). Themes have no documented hook for customer events after a region swap.

The bridge we built, and why we dislike it

The only reliable hook we found runs before content_for_header. snippets/partial-wpm-init-capture.liquid wraps Object.defineProperty, and when Shopify defines window.webPixelsManager it wraps init and stashes api.publish as window.__shopifyWpmPublish.

After each partial apply, assets/partial-pixel-events.js parses the destination bootstrap from fetched HTML and replays Trekkie for parity. It reads view-event-payload from collection-component or product-component, calls __shopifyWpmPublish('page_viewed', …) plus template events, and publishes horizon_partials:navigation as a custom event for debugging.

With that in place, app pixel events fire after soft nav, and localStorage.setItem('horizon-partials-pixel-debug', 'true') shows confirm logs in the main console.

We would not ship this approach on a merchant store without explicit measurement trade-offs. The bridge intercepts a platform global, parses undocumented inline script shapes, and assumes WPM payload contracts stay stable, so one CDN deploy that changes loader order, property descriptors, or the Trekkie bootstrap could silence every app pixel on soft nav with no deprecation notice.

Stores where app sprawl already complicates the header have enough fragile integration points without adding another one.

The rest stays under theme control. Collection and product view events flow from collection-component and product-component built with @shopify/events, cart follows documented patterns, and the gap is specific to customer events for web pixels on route changes that are not full document loads.

What we want from Shopify

Partials are a developer preview, and soft navigation fits where the platform is heading, so measurement belongs in the platform too. Any one of three shapes would retire the bridge:

  1. Bridge standard storefront events to pixel sandboxes on partial navigation: shopify:page:view to page_viewed, and template view events to product_viewed or collection_viewed, with correct payloads and consent checks.
  2. Document a supported API such as Shopify.analytics.publishStandardEvent(name, data) callable after partials.apply() without re-initing WPM.
  3. Emit pixel events from StorefrontRenderer when it serves partial HTML responses.

Until one of those ships, plan measurement before you ship soft navigation. Accept stale pixel counts on in-app routes, fall back to full navigation where attribution matters, or publish custom events knowing app pixels only see them if they subscribe to those names.

Conclusion

You can now walk the front door clicker on a Liquid partials storefront: name the two buses, see why shopify:page:view can look healthy while page_viewed stays silent, and decide whether to accept stale pixel counts, fall back to full navigation where attribution matters, or wait for first-party support.

On our preview benchmarks, repeat route changes dropped from a 1,187 ms median full reload to 49 ms for a partial swap, with CLS unchanged and a modest first-load regression from the navigation runtime. The pixel gap is real, separable, and solvable at the platform layer. Our bridge proves events can fire after a region swap if you reach WPM's internal publish function, and it also proves themes should not have to.

Until Shopify fits a clicker to every aisle, send buyers back through the front door where the count has to be trusted. Source and a live demo are in the open-source horizon-partials repo; the liquid-july preview store is password-protected (password: ocontis). For layout boundaries, partial strategy, and analytics ownership on an established store, see our Shopify theme development capability or book a stack assessment to map preview features against what your stack can maintain and measure long term.

Experimental open-source fork of Horizon 4.1.3, which requires the Liquid July '26 feature preview.

Frequently Asked Questions

On a full page load, content_for_header initialises Web Pixels Manager (WPM) once and pixel sandboxes receive standard customer events such as page_viewed. Soft navigation swaps a named Liquid partial without reloading the document, so WPM does not re-init. Dispatching shopify:page:view updates the standard storefront event bus, but that pipe does not automatically reach pixel iframes. Trekkie replay can update analytics meta and Monorail telemetry, yet it does not call WPM's internal publish() for customer events.

shopify:page:view is a standard storefront event themes and apps can listen for. It appears in the standard-events inspector when emitted after a partial apply. page_viewed is a web pixel customer event delivered through WPM to Meta, Google, Klaviyo, session replay, and other installed pixels. They serve different listeners. Firing the first after soft navigation does not guarantee the second reaches pixel sandboxes without platform support or an unsupported theme bridge.

On the buyer-facing storefront, Shopify.analytics.publish is wired to publishCustomEvent only. Standard event names such as page_viewed return false. Public documentation does not describe a supported theme API for emitting standard pixel customer events after partials.apply(). Custom event names can be published, but app pixels subscribed to standard events will not see them unless they explicitly listen for those custom names.

Partially. Parsing destination inline scripts for ShopifyAnalytics.meta and replaying ShopifyAnalytics.lib.page() and lib.track() updates analytics state and emits Monorail parity events. Post-init Trekkie shim handlers produce telemetry payloads, not WPM publish('page_viewed', …) calls. Pixel sandboxes remain silent on the standard customer event path even when meta and Trekkie replay look correct in the top-level console.

We treat it as experimental only. Capturing WPM's internal publish before content_for_header freezes the global depends on loader order, property descriptors, and undocumented inline script shapes. Shopify can change any of those in a CDN deploy without a theme-facing deprecation path. Teams shipping soft navigation on production should plan measurement explicitly: accept stale pixel counts on in-app routes, fall back to full navigation where attribution matters, or wait for first-party platform support.

Bring in theme engineering when soft navigation must stay accessible, market-aware, and measurable across checkout extensions, app pixels, and reporting workflows your finance team trusts. Partials reduce reload cost but they expose lifecycle gaps full page loads hide. A structured Shopify stack audit helps decide whether persistent shell navigation belongs in your roadmap and how to handle attribution until Shopify ships supported pixel events on region swaps.

On our Horizon fork, first-visit Lighthouse scores were modestly lower with partials enabled: LCP rose by about 1.3 seconds and Total Blocking Time by 85 ms on a local dev benchmark, while CLS stayed identical. The partials commit adds navigation runtime, lifecycle sync, view transitions, and pixel bridge code that runs once on cold load. Repeat in-store navigation was ~24× faster because only the page-content region swaps. Treat absolute dev-server seconds as directional; the meaningful signal is the trade-off between slightly slower first paint and dramatically faster browse flows after the shell is mounted.

Related Articles