widget.js is a 6,163-line, 260KB single file that every Stamped merchant loads on every page of their store.
It sometimes loads twice. We've observed this in the wild but the exact cause isn't fully diagnosed. There are at least two mechanisms that can inject it: the Shopify ScriptTag API (registered programmatically) and a manual <script> tag in theme.liquid. The code tries to detect duplicates before creating a new ScriptTag, but it never removes the other copy. There are runtime guards to prevent double-initialization, but by then the browser has already downloaded and parsed the full 107KB a second time. The guard is like locking the front door after the elephant is already in the living room.
It bundles jQuery 1.11 (2014 vintage). On load, it checks if the store already has jQuery. If not, it fetches it from Google's CDN as a blocking network request before anything else can happen. If the store does have jQuery, it hijacks that copy. Either way: a 12-year-old library is the gatekeeper to showing a single star rating.
It bundles LazyLoad, TimeAgo, and Slick Carousel — all inlined as minified blobs. Every store pays the parse cost for all three even if they only show review badges.
The initialization is a chain of blocking steps:
- Parse 260KB of JS
- Scan the DOM for the
<script>tag to extractapiKeyusing.indexOf()on the src - If no API key found, fire a blocking XHR to
stamped.io/api/getappkey - Check if jQuery exists — if not, fetch it from Google CDN
- Wait for jQuery's
document.ready— but if it hasn't fired within 3 seconds, force-start anyway ('Library not started, fallback') - Once started, inject Google Fonts CSS into
<head>(render-blocking) - Initialize everything at once: UGC widgets, reviews, and the full rewards/loyalty system
- The rewards path fires another API call to check if the store has Loyalty 2.0, then another to load the launcher, then builds two iframes (one for the launcher button, one for the rewards window) — each with their own CSS, their own copy of FontAwesome loaded from kit.fontawesome.com, their own Google Fonts link, and a hardcoded 1-second
setTimeoutbefore the rewards window is considered "loaded"
So on a store that uses loyalty: parse 260KB, wait up to 3s for ready, fetch jQuery, fetch the API key, check the loyalty feature flag, fetch the launcher HTML, wait 1 second, build two iframes, load FontAwesome twice, load Google Fonts twice. That's the critical path.
It has an IE compatibility layer. CustomEvent polyfill, document.attachEvent fallback, $.support.cors = true for IE's broken CORS. A mobile detection regex that tests for BlackBerry, Palm OS, and Windows CE. Somewhere, a Palm Pilot is flattered.
It monkey-patches String.prototype globally on every merchant's store.
It's "minified" by the ASP.NET bundler — basic whitespace removal from the .NET Framework era. Modern minifiers like terser (the industry standard — it's what webpack, Vite, and Next.js all use under the hood) go much further: they shorten variable names, eliminate dead code paths, collapse constants, and restructure expressions. Running terser on the same source with zero code changes would produce a meaningfully smaller file.
It's deployed by hand — edit, run the bundler, upload to S3 via the AWS console, manually invalidate CloudFront. The repo's own README warns the S3 file may not match what's in git. We have two sources of truth, which is another way of saying we have zero.
No versioning. One URL for every merchant, every version, forever.
The script is parser-blocking — the browser can't render anything below the <script> tag until the entire 107KB is downloaded, parsed, and executed. Then it kicks off the init chain: potentially two more network round-trips (getAppKey + jQuery) before it even starts asking for widget data.
We don't need a rewrite. We could start ultra-conservatively — pick one thing, ship a widget.v2.js alongside the current file, and let merchants try it:
-
Just remove jQuery. The 71 calls are all replaceable with
fetch,URLSearchParams,querySelector,innerHTML. That alone cuts a blocking network round-trip and removes the dependency on a 2014 library. -
Or even smaller: just add a URL override so we can point any single store at
widget.v2.jsfor testing, without touching everyone else. -
Or just: run the current source through terser and upload that instead of the ASP.NET bundler output.
Any one of these is a day of work, zero risk to existing merchants, and we can roll it out on a store-by-store basis to measure before/after. We can even test locally before touching anything — a browser extension like Resource Override or Requestly can intercept the CDN request and swap in our modified file on any live store, so we see exactly what changes before anyone else does.
Nice overview! Here's my suggestions
We could build, on top of
widget.v2.jsa better bundling strategy too, splitting vendor bundle from the other chunks, and using a long term cache strategy for vendor bundles, and use "cache first -> then revalidate" strategy for bursting the stale cache when necessary, this way, even after removingjQueryas dependency (which I agree that we could do if we're not supporting really old/legacy browsers), we would improve rendering time (specially first contentful paint score). Most modern bundlers support creating such strategies by default on via config property.The entry chunk should be really small and have only the necessary to spin up what's needed at that time, like reading the API key, detecting what's needed on the current page and orchestrating everything else via lazy loading and/or dynamic imports, once that parsing is done, we can use pre-fetching for the rest, if needed. All of this can be done via fetch API, which doesn't block client interaction without using
async/awaitUtility libs can also be splitted on a separate chunk and lazy loaded on demand, which will reduce even more of our initial bundle size, and can be cached with less aggressive strategies as well, which will have a "single cost" per client visit.
From this point on, we can leverage the bundler tool for versioning the build assets, which will generate other positive side-effects: