Widget Load Sequence

Widget Load Sequence

How the storefront widget boots — from the server-rendered button to an open modal — and how we make the “Try it on” button instant and never dead-clickable.

Related: Widget States · Widget Components · Try-On Pipeline.


The three scripts

FileRoleLoad
tryvio-theme.js (~11 KB)Bootstrap. Wires each [data-tryvio-root] (w()), fetches config, lazy-loads the modal on open.Block "javascript" → Shopify emits it defer.
tryvio-modal.js (~118 KB min)The modal UI. Exposes window._TN.runModal.Lazy — injected by T() only when the modal opens (prefetched on hover).
tryvio-collection.jsInjects the same button onto collection/home/product-block cards.defer.

The heavy modal is never on the critical path — it loads on click, behind a loading skeleton (.tryvio-modal.tryvio-loading-skeleton, created by H()).


The problem: deferred script → a click gap

tryvio-theme.js is deferred, so it executes after HTML parse (at DOMContentLoaded). The button is painted much earlier (it’s server-rendered by tryvio-launcher.liquid). Between paint and w() wiring the click handler there is a window whose size = the script’s download+exec time (hundreds of ms on slow mobile). The old design hid this by keeping the button opacity:0 until w() ran and the /api/proxy config returned — i.e. the button only became clickable once fully wired. That removed dead clicks but also made the button flash hidden for a whole network round-trip.

The fix: reveal-first + an inline micro-bootstrap (B22)

The button is visible from the first paint and interactive immediately, with the heavy work behind it.

parse ─┬─ button painted (visible: in-stock) ─┬─────────────────────────► modal opens
       ├─ inline micro-bootstrap runs         │  (over the loading skeleton)
       │    ├─ config fetch kicked (eager) ────┼──► config ready
       │    └─ click listeners armed          │
       │                                       │
   [early click] ─► spinner + pending flag ────┘  (deferred tryvio-theme.js → w() → replays the click)

1. Liquid — reveal-first + sold-out (server side)

snippets/tryvio-launcher.liquid renders the button visible (no JS gate). It knows availability, so a sold-out product renders the button hidden server-side with .tryvio-product-button--soldout-hidden (CSS display:none) — matching the hideOnSoldOut default (ON), so it never flashes.

2. Inline micro-bootstrap — runs during parse

A tiny <script> inlined right after the button (so it runs before the deferred tryvio-theme.js):

  • Eager config prefetch — reads data-proxy-url, kicks the fetch now, stashes the promise on window.__tvCfg[url]. The config request is in flight from parse time, parallel with page load.
  • Catch early clicks — capture-phase pointerdown/click on the trigger. If the root is not yet bootstrapped (data-tryvio-bootstrapped !== "true"): preventDefault, add the existing --loading spinner (instant feedback), set data-tryvio-pending-open="1". No dead click.
  • Idempotent; de-dupes against the main bundle via data-tryvio-bootstrapped.

3. tryvio-theme.js w() — reveal-first, eager reuse, replay

  • Reveal-first: the opacity:0 gate now applies only to autoplace (if (d)), so the launcher button is never hidden by JS.
  • Eager reuse: k(url) awaits window.__tvCfg[url] (the inline prefetch) before doing its own fetch; the 30 s sessionStorage cache is the second tier.
  • Replay: after wiring the real handler, if data-tryvio-pending-open === "1" it clears the flag + spinner and dispatches the click → the modal opens. This joins the existing during-config queued-click (c) so both pre-w() and during-config clicks are honoured.
  • Sold-out reveal: on reveal it clears --soldout-hidden; so a store with hideOnSoldOut OFF shows the button on sold-out products (try-on still works; the modal surfaces the sold-out state — the cart-add is blocked by the B2 path).

window.__tvCfg is written by the inline bootstrap and read by the same-version bundle. An older cached tryvio-theme.js simply ignores it and does its own fetch — safe across widget versions.


Cards (collection / home / product-blocks)

Card buttons are injected by tryvio-collection.js after two lightweight calls (page-config + batch-status — one each per page, not per card). Each injected root is picked up by tryvio-theme.js’s MutationObserver, but through the lazy gate wl(): for --card roots it defers w() (and the per-card /api/proxy) until the first pointerenter/pointerdown — so the heavy proxy fires only for cards the shopper touches, not ~57 at once (B23). The card button is shown immediately and stays visible (wl() restores opacity:1 right after kicking w(), so hover never hides it). Sold-out removal for cards is independent (DOM badge / IntersectionObserver + /products.js).


Backward-compat & rollback

  • API unchanged — additive only; /api/proxy runs its independent reads in parallel (Promise.allSettled) but the response shape is identical.
  • Deploy order — widget-only change (liquid + tryvio-theme.js + CSS). No API dependency.
  • Rollback — re-add n.style.opacity="0" in w() (one line) to restore the gated behaviour; the inline bootstrap then becomes a harmless no-op.

Tests

  • playwright-debug/proof-B22-instant-button.js — delayed tryvio-theme.js; asserts instant-visible, eager config before click, early click → spinner + replay-open, sold-out hidden (5/5).
  • playwright-debug/proof-B23-lazy-card-proxy.js — no proxy burst; lazy per-card fetch (4/4).
  • playwright-debug/e2e-suite.js — full modal flow, 25/25.