Skip to content

Lazy loading and pickup

Many shop modules load the widget only when a shopper first clicks the "Try on" button. That saves a request on every page view, but it breaks one thing: a try-on that was sent on one page must be picked up on the next page, and the widget can only do that once it is loaded. This page explains the problem and the small loader that solves it.

The widget's own options and events are described in the Widget reference.

Why it matters

A try-on that has been sent is paid for, and the widget keeps following it when the shopper closes the window or moves to another page of the same tab (see "A try-on outlives its window" in the Widget reference). The widget keeps a small record of it in the tab's sessionStorage. The next page finds that record when TryOn.init() runs.

If your module loads the widget only on the first click, then on the next page nothing runs init(), so:

  • the shopper who moved to another product sees no sign that their try-on finished;
  • the result stays unclaimed until they happen to click the button again.

The fix is to load the widget at once on any page where the tab holds a record for your key, and to keep loading it lazily everywhere else. To do that you need to know whether such a record exists without loading the widget. That is what the loader is for.

The loader

<script src="https://api.ziex-tryon.com/embed/tryon-loader.js"></script>

The loader is a few kilobytes (ES5, no dependencies) and is served next to the widget with the same caching. It adds one global object, ZiexTryOn, with two functions and a version property (the loader's own version; the widget's is TryOn.version).

ZiexTryOn.hasPending(pk)

ZiexTryOn.hasPending('pk_YOUR_PUBLISHABLE_KEY'); // true | false

Returns true when the current tab holds a record of a try-on sent with this publishable key: the try-on is still running, or it has finished and its outcome has not been shown yet. It returns false otherwise, including when sessionStorage is not available. It reads storage only; it makes no request and loads nothing.

ZiexTryOn.load([scriptUrl])

ZiexTryOn.load().then(function (TryOn) {
  TryOn.init({ apiKey: 'pk_YOUR_PUBLISHABLE_KEY' });
});

Loads the widget script once and returns a promise that resolves with window.TryOn. Repeated calls share the same load and resolve with the same object, whatever their argument. Without an argument it loads tryon-embed.js from the loader's own directory, which is https://api.ziex-tryon.com/embed/tryon-embed.js when you print the loader as shown above (and also when an optimiser has inlined or renamed the loader); pass scriptUrl only if you serve the widget from another address. Do not print tryon-embed.js yourself as well.

If the script cannot be loaded (an HTTP error, a network failure, a script without TryOn) or does not arrive within 30 seconds, the promise is rejected, so add a .catch() if you want to react. The next call tries again; after a timeout it waits for the same script tag, so the widget never runs twice. The widget needs Promise; in a browser without it load() throws.

load() does not call init(). That stays with your page, so you decide the configuration.

The pattern

Print the loader on every page that can show the button. On each page, ask hasPending() and load at once when it is true; otherwise load on the first click.

<button type="button" id="my-tryon-button" class="my-tryon-button">Try on</button>

<script src="https://api.ziex-tryon.com/embed/tryon-loader.js"></script>
<script>
  (function () {
    var KEY = 'pk_YOUR_PUBLISHABLE_KEY';
    var config = {
      apiKey: KEY,
      launcher: false,                     // your own button opens the window
      lockCategory: true,
      category: 'tops',
      productImage: 'https://shop.example/img/blue-shirt.jpg',
      productTitle: 'Blue shirt',
      productUrl: 'https://shop.example/blue-shirt',
      integration: 'my-shop-module/1.0.0'
    };

    var ready = null;
    function widget() {
      if (!ready) {
        ready = ZiexTryOn.load().then(function (TryOn) {
          TryOn.init(config);              // init() also picks up a sent try-on
          return TryOn;
        });
      }
      return ready;
    }

    // A try-on sent on an earlier page of this tab: load now, so it is picked up.
    if (ZiexTryOn.hasPending(KEY)) widget();

    // Everywhere else: load on the first click.
    document.getElementById('my-tryon-button').addEventListener('click', function () {
      widget().then(function (TryOn) { TryOn.open(); });
    });
  })();
</script>

Things to get right:

  • Call init() with the same publishable key on every page. The record belongs to a key; a page that uses another key does not see it.
  • Call init() on every page where hasPending() is true, even when the page does not show a product. The pickup is done by init().
  • init() does not open the window. The shopper opens it, or your page does, and the outcome is shown then.
  • A single-page site that switches products should call TryOn.update(), not destroy() and init(); see update() or destroy().

Telling the shopper that a result is waiting

While an outcome waits for the next opening of the window, the widget does two things your button can use.

1. A flag on <html>. data-tryon-ready is set to result or error. It stays set on every later page of the tab until the shopper opens the window and sees the outcome. With launcher: false the widget draws no button, so style your own:

.my-tryon-button { position: relative; }

html[data-tryon-ready] .my-tryon-button::after {
  content: "";
  position: absolute;
  top: -3px;
  right: -3px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #16a34a;               /* a result is ready */
}
html[data-tryon-ready="error"] .my-tryon-button::after {
  background: #dc2626;               /* the try-on failed */
}

Because the flag is plain CSS on the root element, the badge appears even before any script of yours has run on the page.

2. The ready event. It is emitted once, on the page where the outcome arrived, after the outcome's own upload_completed, result or error events:

TryOn.on('ready', function (e) {
  // e = { job_id: '…', kind: 'result' | 'error', background: true }
  console.log('Try-on outcome waiting:', e.kind);
});

Later pages do not emit it again; they show the flag only. Use the flag for the page-to-page state and the event for one-off reactions such as analytics.

What is stored, and for how long

  • The record lives in the tab's sessionStorage. It is not shared with other tabs, and it is gone when the tab is closed. A closed tab means the result is visible only in the client portal and the job history, not on your site.
  • It holds the idempotency key of the sent try-on, the job id once known, when it was sent, the estimate, the category and the product (name, link, picture address). It never holds the shopper's photo or your API key.
  • It is removed when the outcome is shown, when the widget confirms that nothing was created, after attemptMaxMs (45 minutes by default), or when you call TryOn.destroy().

Use the loader, do not copy the rule

The record's name is derived from the key by a rule inside the widget. Do not re-implement it in your module to check for a record yourself. Call ZiexTryOn.hasPending(). The loader and the widget are released together, so the answer always matches what the widget would find.

What is frozen in version 1

These parts of the contract do not change within version 1, so a module that relies on them keeps working:

  • the storage slot in sessionStorage: the name ziex-tryon:attempt: followed by the FNV-1a (32-bit) hash of the publishable key, written as 8 lower-case hexadecimal digits;
  • the record's version, v: 1;
  • the flag html[data-tryon-ready] with the values result and error;
  • the ready event and its data { job_id, kind, background };
  • the loader's interface: ZiexTryOn.hasPending(pk) and ZiexTryOn.load([scriptUrl]).

A change to any of them is made only as a new version of the record that old and new code both read, and it is announced at least 90 days in advance. The version of the widget itself is TryOn.version, a semver string, and its options and events only grow within a major version.