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
| File | Role | Load |
|---|---|---|
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.js | Injects 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 onwindow.__tvCfg[url]. The config request is in flight from parse time, parallel with page load. - Catch early clicks — capture-phase
pointerdown/clickon the trigger. If the root is not yet bootstrapped (data-tryvio-bootstrapped !== "true"):preventDefault, add the existing--loadingspinner (instant feedback), setdata-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:0gate now applies only to autoplace (if (d)), so the launcher button is never hidden by JS. - Eager reuse:
k(url)awaitswindow.__tvCfg[url](the inline prefetch) before doing its own fetch; the 30 ssessionStoragecache 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 withhideOnSoldOutOFF 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/proxyruns 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"inw()(one line) to restore the gated behaviour; the inline bootstrap then becomes a harmless no-op.
Tests
playwright-debug/proof-B22-instant-button.js— delayedtryvio-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.