Deferred LyveCom Shoppable-Video Mounting on the Product Page
An integration layer that loads LyveCom's widget script once per page and mounts each carousel only when its container is about to enter the viewport — so a paid shoppable-video app stays off the product page's first paint, and a theme-editor reload doesn't mount it twice.
A shoppable-video app's stock embed loads its bundle and initializes every carousel on page load, which puts a third-party runtime on the mobile product page's critical path. This loader memoizes one promise for the vendor script, adopts the tag if an app embed already added it, and mounts each carousel only when its container scrolls into view — surviving theme-editor re-renders without mounting anything twice.
The Problem
Shoppable video on a product page is a third-party runtime by definition: the player, the product tagging and the carousel all live with the vendor, and the theme's job is to give them a place to mount. LyveCom's default embed does that the simple way — its script loads with the page and initializes every carousel it finds, immediately. On a desktop that's a cost. On a mobile product page it lands squarely on the critical path, in front of the gallery, the price and the add-to-cart button.
Removing the video wasn't the answer. "See it in action" footage of real curls is the kind of social proof that moves a hair-care shopper, and the client had brought the app in for it. The problem was when it loaded, not whether.
The Constraint
The widget is a third-party runtime served from the vendor's own domain. It can't be pulled into the theme's esbuild pipeline, bundled, or versioned alongside the theme's own assets; the theme has to ask for it at runtime. And the vendor's global, LyveComWidget, can appear at any time — the app's own embed may have injected the script before the theme's code runs, or it may not have. The loader had to work either way, and never end up with two copies of the vendor bundle on one page.
The theme editor re-renders sections in place. Any mount logic has to survive shopify:section:load — a merchandiser editing the product template gets a fresh copy of the section's markup, and the carousel has to mount into it — but it must never mount the same node twice, because the vendor's mount call isn't idempotent on our behalf.
What We Built
see-it-in-action-lyvecom.js is an IIFE that owns two things: a single memoized promise for the vendor script, and a viewport observer for the mount points.
One script, one promise. The promise resolves immediately if LyveComWidget already exists on the page. Otherwise it looks for an existing <script src> tag for the vendor bundle and adopts it, tracking readiness through a data-lyve-loaded flag on the tag; only if there's no tag at all does it inject one. Every mount waits on that same promise, so several carousels on one page share one load and the vendor bundle is requested at most once, regardless of who requested it first.
Mount on approach. Mount points are found by the [data-see-it-lyvecom] attribute and handed to an IntersectionObserver with a rootMargin of a couple of hundred pixels and a threshold of 0.01, so a carousel starts mounting just before it scrolls into view rather than the moment the page loads. The observer unobserves each node on its first intersection. When the script promise resolves for that node, the loader calls LyveComWidget.mountCarouselComponent with the node's carousel id and its element as parentElement, and marks the node as mounted. Browsers without IntersectionObserver fall back to mounting everything once the script has loaded.
Liquid gates the request. see_it_in_action.liquid renders the mount point with a per-block id, lyvecom-mount-<block id>, and the carousel id from the block's settings. de_main_product.liquid enqueues the minified loader only when a see_it_in_action block exists on the template with a non-blank carousel_id. A product template without the block never requests the loader, let alone the vendor bundle.
Why This Way
Two gates, at two layers, for two different costs. The Liquid gate means a product page without the block pays nothing — no loader, no vendor script, no observer. The intersection gate means a product page with the block pays for the vendor script only when a shopper scrolls toward the carousel, which on a mobile PDP is well after the gallery and the buy box have painted. The first paint never carries the widget.
Memoizing the script promise, and adopting an existing tag rather than always injecting one, is what lets this coexist with the app's own embed. Whichever runs first, there's one bundle.
You pay for it in a way a shopper can see. A shopper who scrolls fast reaches an empty placeholder for a moment while the vendor script loads and the carousel mounts; the stock embed, having paid up front, would have had it ready. And we now own a loader for someone else's script: the mount call, its argument shape and the global it hangs off are the vendor's API, and a change on their side is a change we have to track.
Why Not an App
This is the opposite of replacing an app. LyveCom is a paid app, it renders the video experience, and none of that is ours. What we built is the integration between a purchased widget and a theme that cares about its first paint: a loader that makes the app's runtime arrive late, once, and survive the theme editor. The app's own embed is the alternative, and it's what you get when nobody writes this.
Implementation Notes
- Idempotency is enforced twice:
dataset.lyvecomMountedon each node, so a node is mounted once, and a module-levelscriptPromise, so the vendor script is requested once. document.addEventListener('shopify:section:load', …)re-scans only the reloaded section's subtree, so a theme-editor edit re-mounts the carousel in that section without touching others.- A mount aborts early if the node has no
idor nodata-carousel-id, because the vendor's mount call needs a CSS selector for itsparentElement. - Every promise rejection is swallowed, so a vendor outage produces an empty placeholder rather than an exception in the product page.
- The loader is enqueued from
de_main_product.liquidonly when thesee_it_in_actionblock is present and configured; the block'scarousel_idsetting carries the id of a carousel assembled in the vendor's dashboard.
Edge Cases
- A browser without
IntersectionObservermounts every carousel eagerly after the script loads, so old browsers get the stock behavior rather than no video. - A vendor script tag already on the page — injected by the app embed, say — is adopted and waited on rather than duplicated.
- A script that fails to load leaves the placeholder empty instead of breaking the product page.
Platform Primitives Used
app-blocks— the mount point is a theme block on the product template whose settings carry the vendor's carousel id; the app's own embed and this block have to coexist.intersection-observer— the gate between "the page loaded" and "the shopper is near the carousel," with a root margin so the mount starts just ahead of the scroll.
Integrations in Play
- LyveCom — the shoppable-video app. It owns the player, the carousel and the product tagging; this loader wraps its script and decides when its mount call runs.
Where It Runs
On the product detail page, mounted by all four product templates: the default product template, the 2026 redesign template, the kids redesign template and the waiting-list template. The block is enabled on each, so every product page carries the mount point — and, because of the Liquid gate, only the ones with a configured carousel id load anything.
What This Demonstrates
- Video and shoppable media modules — the primary capability, on its integration side: a shoppable-video vendor bridged into the theme on the theme's terms.
- Vendor widget remediation and cross-app cart sync — a purchased widget made to load late, load once and survive the theme editor, which is the recurring cost of the buy decision made visible.
- Core Web Vitals and media performance — viewport-gated hydration of a third-party runtime, so the product page's first paint is the theme's own.
How We Know
Two JavaScript assets — the loader and its minified build — and one Liquid snippet, roughly 150 lines together, plus four client working-session records and a metafield export from spring 2026. The records establish that LyveCom arrived as a client-supplied app to be integrated rather than as an engineering choice, that the homepage placement needed no design mockups, and that a carousel id already lived in a product metafield before the rebrand — the app was in use, and the work here was to change how it loads. The loader's mechanism is read from the code.
Related Builds
- Shoppable-video to cart bridge: Hue add-to-cart event wired into Rebuy's drawer — a different storefront's integration with a different shoppable-video vendor, where the seam is the cart rather than the load.
- Alt-text driven variant media gallery with tiered matching and full-screen lightbox — the gallery that owns the first paint this loader stays out of, on the same four product templates.
- Detached mobile drawer with three-level native-details navigation — the same storefront's other piece of vendor-widget work, where a purchased component is repositioned rather than deferred.
The Buy-vs-Build Question
Shoppable video is a buy: the player, the hosting and the in-video product cards are not worth writing. What's not for sale is how the vendor's runtime meets your theme — and a loader of this size is a small price for keeping a paid widget off the first paint. Where the line sits between the player you buy and the integration you own is the subject of Shoppable video and media: buy or build?
Provenance & Evidence
- Client: LUS Brands — loveurcurls.com
- Surface: Product detail page
- Templates served: four — the default product template, the 2026 redesign template, the kids redesign template and the waiting-list template
- Complexity: Medium
- Attribution: Deploi-authored integration layer. The loader, the snippet and the conditional enqueue are ours; the video experience, the player and the carousel are LyveCom's, a paid app. It runs in a theme built on Dawn 15.4.1.
- Status: Live, verified 2026-09-06
- Evidence: Two JavaScript assets and one Liquid snippet, plus four client working-session records and a metafield export from spring 2026
- Confidence: Strong — the records document the app's arrival and prior use; the loader is documented from the code
- Primary capability: Video and shoppable media modules
Ready to Keep a Paid Video App Off Your Critical Path?
You dream it. We build it. If a shoppable-video widget is the first thing your mobile product page waits for, Contact us today and we'll show you what it takes to make it arrive second.