Nostra / Course

The Shopify Speed Playbook

Seven modules. Actionable in an afternoon.

The Shopify Speed Playbook

Make your store faster. Today.

Every tactic in Modules 01–06 is something you can do without a new vendor, without a developer on call, and without a platform migration. We cover render-blocking scripts, image delivery, ghost app code, font rendering, critical CSS, and third-party script management, then show you how Nostra's Edge Delivery Engine takes your store beyond what manual optimization can achieve.

We wrote this because most "site speed" content is either too abstract (Core Web Vitals explainers that don't tell you what to change) or too vendor-specific (here's why you need our product). This is neither. It's a practical checklist with code you can copy.

Module 01

Kill your render-blocking scripts

Your Shopify store stops rendering (completely stops) every time the browser hits a <script> tag without a defer attribute. Not slows down. Stops. If you have 10 apps installed, that could be 10 full stops before your customer sees a single product image. This module shows you exactly how to find them and fix them.

01.1

Understand what "render-blocking" actually means

Concept — 5 min

Browsers render HTML sequentially, top to bottom. When the parser hits a <script> tag, it stops completely. It has to: the script might call document.write(), which would change the structure of the page the parser is building. So by default, the browser stops, downloads the script, executes it front to back, then resumes reading the HTML.

This default behavior made sense in 2005. In 2026, it means every analytics tag, chat widget, loyalty script, and review badge you've added to your store is blocking your page from rendering.

The two attributes that fix this:

The difference between defer and async
/* BLOCKING — default behavior, stops parsing */
<script src="app.com/widget.js"></script>
/* DEFER — downloads in parallel, runs after HTML is parsed */
/* Maintains execution order. Right choice for most scripts. */
<script src="app.com/widget.js" defer></script>
/* ASYNC — downloads in parallel, runs immediately when ready */
/* Does NOT maintain order. Use only for truly independent scripts. */
<script src="app.com/widget.js" async></script>

Use defer by default. Use async only for scripts that are completely independent and don't rely on other scripts (like an analytics beacon that just fires and forgets). When in doubt, defer is safer.

Important caveat

Not every script can be deferred. Some scripts are crucial for initial page load: A/B testing tools that need to modify the DOM before it paints, consent management banners required by law, Shopify's own theme scripts, and any script that other scripts depend on being available immediately. Deferring these will break functionality. Always test in a preview theme after adding defer or async to any script, and roll changes back if something stops working.

01.2

Audit your theme.liquid head section

High impact
20–30 min

Go to Shopify Admin → Online Store → Themes → Edit Code → Layout → theme.liquid. Everything between <head> and </head> loads on every single page of your store. Count every <script> tag. For each one, ask three questions:

1. Does this script need to run before the page renders? Almost nothing does. Exceptions: Google Tag Manager (sometimes; see below), A/B test scripts that need to hide content before showing it, and Shopify's own theme scripts.

2. Is this from a third-party domain? Chat widgets, review apps, loyalty programs, and exit-intent popups. None of these need to block your render.

3. Does it already have defer or async? Some apps add their scripts correctly. Credit them and move on.

Before — blocking head (what most stores look like)
<head>
  <script src="https://cdn.gorgias.io/gorgias.js"></script>
  <script src="https://app.yotpo.com/loader.js"></script>
  <script src="https://static.klaviyo.com/onsite/js/klaviyo.js"></script>
  <script src="https://cdn.privy.io/privy-v2.js"></script>
  <script src="https://rewards.smile.io/smile.js"></script>
</head>
After — deferred, non-blocking
<head>
  <script src="https://cdn.gorgias.io/gorgias.js" defer></script>
  <script src="https://app.yotpo.com/loader.js" defer></script>
  <script src="https://static.klaviyo.com/onsite/js/klaviyo.js" defer></script>
  <script src="https://cdn.privy.io/privy-v2.js" defer></script>
  <script src="https://rewards.smile.io/smile.js" defer></script>
</head>
Concept — 5 min

Don't defer scripts that write content inline on load. Some older A/B testing tools (legacy Optimizely, VWO) use document.write() and must stay blocking. Change one script at a time and test visually before moving to the next.

01.3

Move non-critical scripts to the bottom of body

High impact
15 min

Even with defer, scripts in <head> still create DNS lookups and connection overhead early in the page load cycle. For scripts that genuinely don't need to run until after the page is interactive (chat widgets, popup tools, feedback buttons) move them entirely out of the head and place them just before the closing </body> tag.

Your customers won't notice that the chat bubble appeared 200ms after the rest of the page. They will notice if your hero image took an extra 800ms to load.

Move to end of body
  <!-- Remove from <head>, place here instead -->
  <script src="https://cdn.gorgias.io/gorgias.js" defer></script>
  <script src="https://cdn.privy.io/privy-v2.js" defer></script>
</body>

Scripts that should stay in <head> (with defer): anything that needs to be available early in page interaction, like Klaviyo's onsite messaging if it targets elements that appear quickly. When uncertain, try it in the body first; if nothing breaks after 10 minutes of browsing, it belongs there.

01.4

Use Chrome DevTools to see exactly what's blocking

Diagnostic — 20 min

Open your store in Chrome, then open DevTools (F12 or Cmd+Option+I). Go to the Network tab. Before you reload, throttle to simulate real mobile conditions: click the gear icon → set CPU throttling to 4x slowdown, Network to Fast 3G. Then reload.

Filter by JS. Sort by Waterfall. You'll see a visual timeline of every script loading. Render-blocking scripts show as thick bars at the very beginning of the waterfall, with everything else queued behind them.

Look for three patterns:

Long bars starting at t=0: These are blocking your initial render. Every millisecond of width on those bars is time your customer is staring at a blank screen.

Sequential chains: Script B doesn't start until Script A finishes, which doesn't start until Script C finishes. This happens when scripts depend on each other and can only be fixed by refactoring, but knowing it exists is the first step.

Red/orange bars: Failed requests. These are ghost scripts from uninstalled apps (covered in Module 03). A failed request still consumes time: the browser opens a connection, waits for a response or timeout, gets an error, and moves on. Sometimes 1–3 seconds of "wait" for nothing.

Pro tip

Run this on mobile device simulation, not desktop. Click the device icon in DevTools toolbar and select a mid-range Android device. Your desktop experience is always faster. Your customers are predominantly on mobile.

Module 01: Checklist
Opened theme.liquid and counted all script tags in <head>
Added defer to all non-critical third-party scripts
Moved chat widget and popup scripts to bottom of body
Ran Chrome DevTools audit with mobile throttling
Verified nothing broke visually after each change
Module 02

Images and video: your biggest payload problem

Images and video are responsible for 60–80% of total page weight on most Shopify stores. The good news: Shopify's CDN handles most of the hard work automatically, but only if you request it correctly. Most themes don't. Here's how to fix that.

02.1

Use image_url with explicit width parameters

High impact
1–2 hrs

When you upload an image to Shopify, it automatically generates multiple sizes: 100px, 200px, 400px, 800px, 1200px, 2048px, and the original. It stores all of these on its CDN. But your theme has to request the right one. If it requests without a size, it gets the full original.

A product photo uploaded at 3000×3000px and 4MB will load that full 4MB file on a mobile device displaying it at 400px wide unless your theme specifies otherwise.

theme — product image, before
{%- comment -%} Requests full-resolution original every time {%- endcomment -%}
<img src="{{ product.featured_image | image_url }}"
     alt="{{ product.featured_image.alt }}">
theme — product image, after (with srcset)
<img
  src="{{ product.featured_image | image_url: width: 800 }}"
  srcset="
    {{ product.featured_image | image_url: width: 400 }} 400w,
    {{ product.featured_image | image_url: width: 800 }} 800w,
    {{ product.featured_image | image_url: width: 1200 }} 1200w,
    {{ product.featured_image | image_url: width: 1600 }} 1600w"
  sizes="(max-width: 640px) 400px,
          (max-width: 1024px) 800px,
          1200px"
  loading="lazy"
  alt="{{ product.featured_image.alt }}"
  width="{{ product.featured_image.width }}"
  height="{{ product.featured_image.height }}">

The srcset attribute tells the browser which sizes are available. The sizes attribute tells it how wide the image will be rendered at various viewport widths. The browser chooses the most appropriate size automatically. Shopify's CDN also serves WebP automatically when you use image_url; no extra work needed.

Adding width and height attributes prevents layout shift (CLS): the browser reserves the right amount of space before the image loads.

Where to make this change

Search your theme code for all occurrences of | image_url }} without a width parameter. Common locations: product-card.liquid, product-media-gallery.liquid, collection-card.liquid, featured-product.liquid. There can be 10–20 instances depending on theme complexity.

02.2

Preload your LCP image: One line, big payoff

High impact
15 min

LCP (Largest Contentful Paint) is Google's most important Core Web Vitals metric. It measures when the largest visible element on screen finishes loading. On most Shopify homepages, that's the hero image. On PDPs, it's usually the primary product photo.

The problem: the browser doesn't know it needs to load your hero image until it's parsed the HTML, loaded the CSS that references it, and processed any JavaScript that might affect layout. By that time, the image request has already been delayed by hundreds of milliseconds.

A preload hint in your <head> tells the browser to start fetching it immediately, before any of that processing happens:

theme.liquid — add to <head> for homepage hero
{%- if template.name == 'index' -%}
  <link rel="preload"
        as="image"
        href="{{ section.settings.hero_image | image_url: width: 1200 }}"
        imagesrcset="{{ section.settings.hero_image | image_url: width: 600 }} 600w,
                    {{ section.settings.hero_image | image_url: width: 1200 }} 1200w"
        imagesizes="100vw"
        fetchpriority="high">
{%- endif -%}

This one change routinely improves LCP by 300–800ms. It's the highest ROI single line of code you can add to a Shopify theme. You should also add fetchpriority="high" and loading="eager" directly to the hero <img> tag itself and remove loading="lazy" from it. Eager loading tells the browser to fetch the image immediately rather than waiting for intersection checks, and lazy loading on your most important image is contradictory to everything else you're doing here.

02.3

Fix autoplay hero videos: The most common LCP killer

High impact
1–3 hrs depending on setup

A homepage hero video is the most common cause of a failing LCP score. A 15-second brand video at reasonable quality is typically 8–20MB as MP4. The browser starts downloading it immediately. Until something visible loads in front of it, LCP is unresolved, meaning Google is watching your page sit in "loading" state while your 18MB video streams.

Fix 1: Always set a poster image. The poster attribute shows a static image instantly while the video loads. This is your LCP element; it renders immediately and satisfies the metric, while the video continues loading in the background.

Hero video with poster — correct implementation
<video
  autoplay muted loop playsinline
  poster="{{ section.settings.hero_poster | image_url: width: 1200 }}"
  preload="none">
  <source src="{{ section.settings.hero_video_webm }}" type="video/webm">
  <source src="{{ section.settings.hero_video_mp4 }}" type="video/mp4">
</video>

Fix 2: Re-encode to WebM. The same video at the same visual quality is typically 40–60% smaller in WebM format. Chrome, Firefox, and Edge all support it. Safari on iOS supports it as of iOS 16. Use HandBrake (free, desktop app) or Cloudinary's video transformation pipeline to convert. Always include an MP4 fallback for the rare Safari on older iOS.

Fix 3: Don't autoplay on mobile at all. On mobile, autoplay videos eat data and rarely improve conversion; most mobile visitors won't watch them before they've scrolled past. Use JavaScript to detect the viewport width and skip video loading below 768px, showing only the poster image.

Skip video on mobile
<script defer>
  const video = document.querySelector('.hero-video');
  if (video && window.innerWidth > 768) {
    video.src = video.dataset.src;
    video.load();
    video.play();
  }
</script>
Module 02: Checklist
Searched theme for image_url calls without explicit width parameter
Added srcset and sizes to product and collection image tags
Added preload hint for hero image in theme.liquid head
Added loading="lazy" to all below-fold images, removed from hero
Set poster attribute on hero video
Re-encoded hero video to WebM (or removed autoplay on mobile)
Module 03

Find and remove ghost app scripts

When you uninstall a Shopify app, Shopify removes the app from your admin. It does not remove the code the app injected into your theme. That code still runs on every page load, often calling endpoints on servers that no longer expect traffic from you, adding latency for requests that return nothing or fail entirely.

03.1

Compare your installed apps against your theme code

High impact
45–60 min

Start by listing every app currently installed: Shopify Admin → Apps. Write down the domain each app's CDN scripts come from (usually visible in their documentation or settings). Now download your live theme as a zip file: Online Store → Themes → Actions → Download.

Unzip it. In your terminal, run a grep for all external script sources:

Terminal — find all external scripts in your theme
# Run from your unzipped theme directory
grep -r "script" . --include="*.liquid" | grep "src=" | grep -v "shopify" | grep -v "cdn.shopify"
# Also check for link tags loading external CSS
grep -r "stylesheet" . --include="*.liquid" | grep "href=" | grep -v "shopify"

For every domain that shows up, check whether it corresponds to an app you currently have installed. Common ghosts we find on stores that have been live for 2+ years:
Privy (cdn.privy.io): replaced by Klaviyo popups but the script wasn't removed
Justuno (cdn.jst.ai): removed after a popup A/B test, code left behind
Sumo / SumoMe (load.sumo.com): almost always a ghost at this point
Legacy Yotpo (staticw2.yotpo.com): old embed code when switching Yotpo plans
Old loyalty apps (Swell, Loyalty Lion first installs often leave code behind
Previous A/B testing tools (Optimizely, VWO, Convert) these are particularly heavy

Before deleting

Search the full theme for every reference to a ghost script's domain before removing anything. Some apps inject code in multiple places: theme.liquid, snippets, section files. Remove all of them, not just the obvious one.

03.2

Audit Shopify's Customer Events (the hidden script layer)

High impact
20 min

Shopify added a Customer Events system (formerly Web Pixels) that lets apps inject tracking scripts outside of your theme code. These scripts survive theme changes (if you switch themes entirely, your Customer Events pixels come with you. Most people don't know they exist.

Go to Shopify Admin → Settings → Customer Events. You'll see every pixel currently running. For each one, verify it corresponds to an integration you actively use and want. Abandoned cart tools, old survey scripts, removed analytics tools, all of these accumulate here.

Delete any pixel you don't recognize or can't map to a current tool. Each one fires JavaScript on every storefront page load.

Also check

Google Tag Manager deserves its own audit if you use it. GTM containers accumulate dead tags the same way themes accumulate dead scripts. Open your GTM container, look for any tag that hasn't fired in the last 30 days (check in GTM's preview mode or GA4's DebugView), and remove it.

03.3

Use WebPageTest to catch failures you can't see in code

Diagnostic — 20 min

Some ghost scripts don't live in your theme files; they're injected by GTM, by Shopify Customer Events, or by third-party tag managers you've forgotten about. The only way to find these is to watch what actually loads in a live browser session.

Go to webpagetest.org. Run a test on your homepage, collection page, and a product page. In the waterfall results, filter to show all requests and look for:

4xx status codes (red): The script loaded but the endpoint it's calling doesn't exist anymore. Classic ghost app signature: the app is gone but the tracking call still fires.

Long time-to-first-byte from unknown domains: A domain you don't recognize taking 800ms+ to respond. That's someone else's slow server eating into your load time.

Large JS files from third-party domains you don't use: Anything over 100kb from a non-essential vendor is worth questioning.

How to read the WebPageTest waterfall
Each row is one request. The columns show:
DNS lookup   → Time to resolve the domain to an IP address
Connect      → Time to establish TCP connection
TLS          → Time for SSL handshake (HTTPS overhead)
TTFB         → Time waiting for first byte from server
Download     → Time to receive the full response
A ghost script calling a dead server looks like:
DNS: 10ms | Connect: 300ms | TTFB: 2000ms | Error: 404
That 2+ second wait is pure wasted time on every page load.
Module 03: Checklist
Listed all currently installed apps and their CDN domains
Downloaded theme and searched for external script domains
Removed all code for apps no longer installed
Audited Settings → Customer Events, removed stale pixels
Ran WebPageTest and checked for 4xx requests and unknown domains
Audited GTM container for tags not fired in last 30 days
Module 04

Fix your fonts in 15 minutes

Custom fonts are a hidden source of render delay on almost every Shopify store. The browser needs to download your font files before it can display text, and by default most themes handle this in a way that causes a flash of invisible text: your layout loads, but all the text is blank until fonts arrive. Here's how to fix it.

04.1

Add font-display: swap to every @font-face rule

High impact
10 min

Without font-display: swap, browsers using custom fonts exhibit FOIT (Flash of Invisible Text). The browser reserves space for your text, knows it needs a custom font, and shows nothing at all until that font downloads. On a slow connection, that could be 2–4 seconds of blank content areas.

font-display: swap changes the behavior: the browser immediately renders text in the best available system font, then swaps to your custom font when it arrives. Your customers see text immediately. The visual swap is usually imperceptible on fast connections and far better than invisibility on slow ones.

Self-hosted fonts — add font-display to @font-face
@font-face {
  font-family: 'BrandFont';
  src: url('{{ "brand-font.woff2" | asset_url }}') format('woff2'),
       url('{{ "brand-font.woff" | asset_url }}')  format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* Add this */
}
Google Fonts URL — append &display=swap
<!-- Before -->
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700"
      rel="stylesheet">
<!-- After -->
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap"
      rel="stylesheet">

Shopify themes built on the Theme Store may reference fonts through the Shopify Font Picker: check base.css or fonts.css.liquid for @font-face declarations and add font-display: swap to each.

04.2

Add preconnect hints for font servers

High impact
5 min

When your page needs a Google Font, the browser has to: resolve the DNS for fonts.googleapis.com, open a TCP connection, complete the TLS handshake, request the CSS, parse it, then open another connection to fonts.gstatic.com (where the actual font files live) and repeat the process. This chain of sequential operations takes 200–500ms on a fast connection and much longer on mobile.

Preconnect hints tell the browser to open those connections as early as possible, before it even knows it needs fonts, while it's still parsing the head:

theme.liquid — add these as the first link tags in <head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Your font link tag comes after -->
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap"
      rel="stylesheet">

The crossorigin attribute on the gstatic preconnect is required: font requests are cross-origin and the connection needs to be opened with the right security context.

04.3

Self-host your fonts to eliminate the external dependency

Medium impact
1–2 hrs

If you're using Google Fonts, every page load makes a request to Google's servers. You can eliminate that entirely by downloading the font files and serving them from Shopify's CDN alongside your other assets. The request never leaves your infrastructure.

Use google-webfonts-helper (available at gwfh.mranftl.com) to download any Google Font as WOFF2 files. Download only the weights and subsets you actually use: latin only unless you serve non-latin languages, and only the weights that appear in your CSS.

Self-hosting steps
1. Download from google-webfonts-helper:
   Select font → Choose weights (only what you use) → Download zip
2. Upload .woff2 files to Shopify theme assets:
   Online Store → Themes → Edit Code → Assets → Add asset
3. Reference in CSS using Shopify's asset_url filter:
@font-face {
  font-family: 'Lato';
  src: url('{{ "lato-v24-latin-regular.woff2" | asset_url }}') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
4. Remove the Google Fonts <link> tag from theme.liquid entirely.
Worth the effort if

You're using 1–2 fonts with 2–3 weights. If you're loading 5 weights across 3 typefaces, the self-hosting effort is real but so is the opportunity to just use fewer fonts, which is often the better fix anyway.

Module 04: Checklist
Added font-display: swap to all @font-face rules in theme CSS
Appended &display=swap to Google Fonts URL
Added preconnect hints for fonts.googleapis.com and fonts.gstatic.com
Verified no FOIT visible on throttled 3G in DevTools
Module 05

Critical CSS and above-the-fold rendering

Your browser can't paint anything until it has parsed all your CSS. If your stylesheet is 300KB of rules for components the visitor hasn't scrolled to yet, you're making them wait for code they don't need. Extracting and inlining the critical CSS for the initial viewport is one of the most effective ways to improve First Contentful Paint and LCP.

05.1

Understand why CSS is render-blocking

Context

Unlike scripts, which you can defer or async, CSS is render-blocking by design. The browser refuses to paint anything until it has downloaded and parsed every <link rel="stylesheet"> in <head>. This is intentional: painting before CSS is ready would cause a flash of unstyled content (FOUC).

The problem is volume. A typical Shopify theme ships 150–400KB of CSS. Most of that styles components below the fold: footer, product tabs, reviews sections, FAQ accordions. The visitor's browser downloads and parses all of it before rendering a single pixel.

The core idea

Identify the CSS needed to render what's visible in the first viewport (the "critical CSS"), inline it directly in <head>, and defer loading the rest until after the page has painted.

05.2

Extract and inline critical CSS

High impact
1–2 hrs

Tools like Critical (by Addy Osmani) and Penthouse can automatically extract the CSS rules needed for above-the-fold content. They load your page in a headless browser, determine which rules apply to the visible viewport, and output just those rules.

Once you have the critical CSS, inline it in a <style> tag in <head> and load the full stylesheet asynchronously:

theme.liquid — inline critical CSS and defer the rest
<head>
  <!-- Critical CSS inlined directly -->
  <style>
    /* Only rules needed for above-the-fold content */
    .header { ... }
    .hero-banner { ... }
    .hero-banner img { ... }
    .announcement-bar { ... }
  </style>

  <!-- Full stylesheet loaded asynchronously -->
  <link rel="preload" href="{{ 'theme.css' | asset_url }}"
        as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript>
    <link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
  </noscript>
</head>

The preload trick loads the stylesheet in the background without blocking rendering. The onload handler switches it to a real stylesheet once it's downloaded. The <noscript> fallback ensures it still works with JavaScript disabled.

05.3

Audit and remove unused CSS

Medium impact
30–60 min

Before extracting critical CSS, it pays to reduce your total CSS footprint. Most Shopify themes carry significant dead CSS from features you've disabled, sections you don't use, and apps you've uninstalled.

Open Chrome DevTools, go to Sources → Coverage (Ctrl+Shift+P → "Coverage"), reload the page, and look at your CSS files. The red bars show unused bytes. It's common to see 60–80% of a theme's CSS go unused on any given page.

Practical approach

You don't need to remove every unused rule. Focus on the obvious wins: entire component blocks for features you don't use (e.g., if you disabled the blog, remove blog CSS). Even trimming 30% of your stylesheet makes a meaningful difference to parse time.

05.4

Scope page-specific CSS with Liquid conditions

Medium impact
30 min

Shopify loads the same stylesheet on every page. Your product page CSS loads on the homepage. Your blog CSS loads on collection pages. You can use Liquid template conditions to load page-specific stylesheets only where they're needed:

theme.liquid — conditional stylesheet loading
{%- if template.name == 'product' -%}
  <link rel="stylesheet" href="{{ 'product.css' | asset_url }}">
{%- endif -%}

{%- if template.name == 'blog' or template.name == 'article' -%}
  <link rel="stylesheet" href="{{ 'blog.css' | asset_url }}">
{%- endif -%}

This requires splitting your monolithic theme.css into page-specific files, which is more work upfront but keeps each page lean. Modern Shopify 2.0 themes like Dawn already follow this pattern.

Module 05: Checklist
Ran Coverage in DevTools to measure unused CSS percentage
Removed CSS blocks for disabled features and unused sections
Extracted and inlined critical CSS in theme.liquid head
Full stylesheet loads asynchronously via preload pattern
Evaluated splitting CSS into page-specific files with Liquid conditions
Module 06

Critical CSS and above-the-fold rendering

Your browser can't paint anything until it has parsed all your CSS. If your stylesheet is 300KB of rules for components the visitor hasn't scrolled to yet, you're making them wait for code they don't need. Extracting and inlining the critical CSS for the initial viewport is one of the most effective ways to improve First Contentful Paint and LCP.

06.1

Audit your third-party script inventory

High impact
30 min

Open Chrome DevTools, go to the Network tab, reload your page, and filter by "JS." Sort by domain. Every domain that isn't your store or cdn.shopify.com is a third-party script. Common culprits include:

Chat widgets (Intercom, Drift, Tidio, Gorgias), review platforms (Judge.me, Yotpo, Loox, Stamped), loyalty and referral tools (Smile.io, Yotpo Loyalty, ReferralCandy), heatmap and session recording (Hotjar, Lucky Orange, Microsoft Clarity), A/B testing tools (Google Optimize, VWO, Optimizely), and social proof popups (Fomo, Nudgify, ProveSource).

For each script, note the file size, load time, and whether it makes additional requests (many scripts load 3–5 more files after the initial download).

The real cost

A single chat widget typically adds 200–400KB of JavaScript and 3–8 network requests. A reviews widget can add 150–300KB. Stack five or six of these together and you're adding 1MB+ of JavaScript to every page load, regardless of whether the visitor interacts with any of it.

06.2

Use the facade pattern for heavy widgets

High impact
1–2 hrs

The facade pattern is a concept from web performance where you show a lightweight placeholder that looks like the real widget but loads none of its JavaScript until the visitor actually interacts with it. For example, instead of loading your entire chat widget on every page, you'd show a static chat icon. When someone clicks it, the real widget loads on demand.

This concept can work well for things like embedded videos (show a poster image, load the player on click) and some simpler widgets. However, it's not a plug-and-play solution for every third-party tool. Many widgets (especially chat platforms like Gorgias, Intercom, or Zendesk) have complex initialization flows, authentication, session management, and DOM requirements that don't work if you just inject the script tag dynamically. Some will break entirely or lose features like proactive messaging.

Before you try this

Check whether your specific widget provider offers a built-in lazy loading or deferred initialization option. Many modern chat and reviews platforms now ship their own lightweight loaders that handle this properly. If your provider doesn't, test thoroughly in a preview environment before pushing to production. The performance savings are real, but so is the risk of breaking a customer-facing tool.

06.3

 Delay non-essential scripts until after interaction

Medium impact
30 min

Some scripts don't warrant a facade but still don't need to load immediately. Heatmaps, analytics, and session recording tools only need to start capturing after the page is interactive. You can delay them until the first user interaction:

Load scripts on first interaction
<script>
  var loaded = false;
  function loadDeferred() {
    if (loaded) return;
    loaded = true;
    // Hotjar
    var hj = document.createElement('script');
    hj.src = 'https://static.hotjar.com/c/hotjar-XXXXXX.js';
    document.head.appendChild(hj);
    // Add more scripts here as needed
  }
  ['scroll', 'click', 'touchstart', 'mousemove'].forEach(function(evt) {
    window.addEventListener(evt, loadDeferred, { once: true, passive: true });
  });
</script>

This keeps the initial page load clean. The scripts load the moment someone scrolls, clicks, or taps, which is early enough for analytics accuracy but late enough to not block the critical rendering path.

06.4

Evaluate whether each script earns its weight

High impact
15 min

For every third-party script on your store, ask three questions: Are we actively using the data or feature this provides? Could we get the same result from a lighter alternative or a built-in Shopify feature? What is the measurable cost (file size, requests, main thread time) vs. the measurable benefit?

Common wins: replacing a 300KB reviews app with Shopify's native product reviews (if you just need basic star ratings), replacing a heavy countdown timer app with 20 lines of vanilla JavaScript, and removing social proof popups that studies show have diminishing returns after the first few months.

Rule of thumb

If a script adds more than 100KB and you can't point to a specific revenue or conversion impact it's driving, it's a candidate for removal or replacement.

Module 06: Checklist
Inventoried all third-party scripts via Network tab (sorted by domain)
Checked if widget providers offer built-in lazy loading options
Deferred non-essential scripts (analytics, heatmaps) to first interaction
Evaluated each third-party script for measurable ROI vs. performance cost
Removed or replaced scripts that don't justify their weight
Module 07

Everything above and then some

Modules 01–06 are real, high-impact changes you can make today. But they're manual, they require ongoing maintenance, and they only address the code you control. Nostra's Edge Delivery Engine automates the hard parts and goes further than manual optimization ever can.

Everything above and then some

310+ edge locations. AI-powered caching. No code changes. No developer resources on your end.

07.1

Audit your third-party script inventory

Context

The tactics in Modules 01–06 optimize what happens after the browser receives your HTML. That matters; a lot. But they can't change the fundamental physics of how far your server is from your customer.

When someone in Tokyo visits your Shopify store, the request travels to Shopify's origin server, processes, and travels back. That round trip takes time no amount of defer attributes can fix. The same applies to a shopper on a rural mobile connection in Texas, or a customer in Berlin on a congested network.

Time to First Byte (TTFB): the time before the browser receives its very first byte of HTML, is the foundation everything else is built on. If your TTFB is 800ms, your LCP physically cannot be faster than 800ms + image download time. You can optimize every script, compress every image, and preload every font, but that 800ms floor remains.

This is the problem edge delivery solves. Instead of every request traveling to a single origin, your pages are served from whichever of 310+ edge locations is closest to the visitor, typically within 50ms.

07.2

What Nostra does that manual optimization can't

High impact

Nostra's Edge Delivery Engine doesn't replace Modules 01–06: it stacks on top of them. But it also addresses an entire category of speed problems that are impossible to solve by editing theme code:

Speed problem Manual fix (Modules 01–06) With Nostra
Render-blocking scripts Add defer/async, move to body Automated + edge-cached
Oversized images Add srcset, preload LCP image AI-optimized caching at edge
High TTFB (server distance) Not fixable in theme code 310+ edge locations, up to 2× faster TTFB
Dynamic content caching Not possible manually AI identifies what's cacheable in real time
Bot traffic slowing your store Not addressable in theme code Edge Protect blocks bots before they reach origin
Ongoing maintenance Manual: check after every app install Continuous, automated optimization
07.3

The numbers: what stores actually see

Medium impact

Every tactic in Modules 01–06 contributes to faster page loads. But the conversion impact of going from "manually optimized" to "edge-delivered" is where the business case gets hard to ignore. Here's what real Shopify stores measured after adding Nostra:

These aren't lab scores. They're measured business outcomes (conversions, revenue, ROI) from stores that were already running on Shopify and already had reasonable themes. The speed improvement from edge delivery translated directly into money.

07.4

No code changes. No developer resources on your end.

Zero dev lift

Nostra sits in front of your Shopify store at the DNS level. You don't edit your theme, you don't install a heavy app that injects more scripts (ironic, given Module 01), and you don't need a developer.

The Edge Delivery Engine uses AI to analyze your site structure in real time and determine what can be cached at the edge and what needs to hit your origin. Dynamic pages like cart, checkout, and account pages route normally. Static and semi-static content (product pages, collection pages, your homepage) gets served from the nearest of 310+ edge locations.

The result: your TTFB drops from hundreds of milliseconds to single-digit or low double-digit milliseconds for most visitors. That faster foundation makes every other optimization in this course work even better.

The takeaway

Do Modules 01–06: they're free, they're impactful, and they make your store better regardless. But if you want to remove the ceiling on how fast your store can be, edge delivery is what gets you there. Manual optimization improves the code. Nostra improves the infrastructure.

Ready to see how fast your store can actually be?

Explore Nostra's Edge Delivery Engine →
Quiz

What did you learn?

Sixteen questions covering all seven modules. Click an answer to see if you got it right (and why).

🎁Score 100% and win free Nostra swag

Get a perfect score to claim a free Nostra hoodie or water bottle. We'll ship it anywhere.

Module 01: Scripts
When a browser encounters a <script> tag with no attributes while parsing HTML, what does it do?
Correct. A plain <script> tag is render-blocking by default. The browser stops the HTML parser, downloads the file, executes it front to back, then resumes. This is why scripts in <head> without defer delay everything visible on the page.
Module 01: Scripts
What is the key difference between defer and async?
Correct. defer guarantees execution order and waits for parsing to finish before running; safe for most third-party scripts. async runs as soon as it downloads regardless of order, which can break scripts that depend on each other. Use defer by default.
Module 02: Images
You call {{ product.featured_image | image_url }} in your Shopify theme with no width parameter. What image does Shopify serve?
Correct. Without a width parameter, Shopify serves the full-resolution original. A product photo uploaded at 4000×4000px and 5MB gets served at full size to every visitor (including someone on a phone displaying it at 400px wide. Always specify a width and use srcset for responsive delivery.
Module 02: Images
What does adding fetchpriority="high" and a <link rel="preload"> hint for your hero image actually do?
Correct. Normally the browser doesn't discover your hero image until it's parsed the HTML, loaded the CSS, and processed any layout-affecting JS, by which point hundreds of milliseconds have passed. A preload hint in <head> lets the browser start fetching it as early as possible, which is why it routinely improves LCP by 300–800ms.
Module 02: Images
Why is an autoplay hero video so damaging to LCP scores?
Correct. A typical brand video is 8–20MB. It starts downloading immediately on page load. Until something visually larger renders in front of it, the browser hasn't resolved LCP, so Google sees the page as "loading" while your video streams. Setting a poster image gives the browser a fast LCP element to latch onto while the video loads in the background.
Module 03: Ghost scripts
You uninstall an app from Shopify. What happens to the JavaScript it injected into your theme?
Correct. Shopify removes the app from your admin but does not clean up code injected into theme files. The scripts remain active, calling endpoints that may no longer respond. A failed request to a dead server can add 500ms–2s of wait time to every single page load, indefinitely, until you manually remove it.
Module 03: Ghost scripts
Where in Shopify can you find tracking scripts that survive even a complete theme switch?
Correct. Shopify's Customer Events system (formerly Web Pixels) lets apps inject tracking scripts that live outside your theme files entirely. They survive theme changes, theme duplication, and going live with a new theme. Every pixel there runs on every storefront page load, so go audit it now if you haven't.
Module 04: Fonts
What is FOIT, and what CSS property fixes it?
Correct. Without font-display: swap, browsers show nothing where text should be until the custom font file fully downloads, that's FOIT. With swap, the browser immediately renders text in the best available system font, then swaps to the custom font when it arrives. Your customers always see text, even on slow connections.
Module 04: Fonts
You're loading Google Fonts via a <link> tag. What do preconnect hints for fonts.googleapis.com and fonts.gstatic.com actually do?
Correct. Loading a Google Font requires DNS lookup → TCP connection → TLS handshake for both googleapis.com and gstatic.com, then the same for the actual font files. Preconnect hints tell the browser to open those connections as early as possible, while it's still reading the head, so the connection setup time isn't on the critical path when the font is actually needed.
Module 05: Critical CSS
Why is CSS render-blocking, and what is the core technique for preventing it from delaying First Contentful Paint?
Correct. The browser blocks rendering until every <link rel="stylesheet"> in the head is downloaded and parsed. By inlining only the CSS needed for the first viewport and loading the rest via a preload pattern, you let the browser paint immediately with the styles it needs while the full stylesheet downloads in the background.
Module 05: Critical CSS
You open Chrome DevTools Coverage on your store and see that 72% of your theme CSS is unused on the homepage. What's the most practical first step?
Correct. You don't need to eliminate every unused rule. Focus on the big blocks: if you've disabled your blog, remove the blog CSS. If you don't use a wishlist feature, cut that section. Trimming even 30% of your stylesheet meaningfully reduces parse time and speeds up rendering.
Module 06: Third-party scripts
What is the "facade pattern" for third-party widgets, and why does it improve performance?
Correct. A facade is a static stand-in that looks like the real widget but carries none of its JavaScript weight. When someone clicks the chat icon, that's when the full widget loads. This keeps the initial page load clean and saves 200–400KB of JavaScript from loading on every page for every visitor, most of whom never open the chat.
Module 06: Third-party scripts
You stack a chat widget, a reviews app, a loyalty popup, and a heatmap tool on your store. Roughly how much JavaScript are you adding to every page load?
Correct. A single chat widget is typically 200–400KB with 3–8 network requests. A reviews widget adds 150–300KB. Stack four or five of these together and you're looking at over 1MB of JavaScript and dozens of network requests on every page load, regardless of whether the visitor interacts with any of them. This is why auditing and deferring third-party scripts is so impactful.
Module 07: Nostra
You've deferred all scripts, optimized every image, and removed ghost code. Your TTFB is still 600ms. Why can't manual theme optimization fix this?
Correct. TTFB measures the time before the browser receives its first byte of HTML. That time is mostly spent in network transit between the visitor and Shopify's origin server. No amount of script deferral or image optimization can reduce the physical distance a request has to travel. This is why edge delivery (serving content from 310+ locations close to the visitor) can improve TTFB by up to 2 times.
Module 07: Nostra
What does Nostra's Edge Delivery Engine do that makes it different from the manual optimizations in Modules 01 through 06?
Correct. Nostra works at the infrastructure level, not the theme level. It sits in front of your store at the DNS layer and serves cached pages from whichever of its 310+ edge locations is closest to the visitor. Its AI identifies which dynamic content can be cached in real time. This reduces TTFB, speeds up total page load, and requires no code changes or developer involvement to set up.
All modules
You make a speed improvement to your store and want to check if it worked. Which Chrome DevTools panel shows you a visual timeline of every resource loading in sequence?
Correct. The Network tab's Waterfall view shows every request the page makes as a horizontal bar on a timeline. You can see exactly when each resource starts loading, how long each phase takes (DNS, connection, TLS, wait, download), and which resources are blocking others. Always run this with mobile throttling enabled (Fast 3G + 4x CPU slowdown) to simulate real conditions.
Claim your free Nostra swag
Perfect score! Pick your prize and tell us where to send it.
🎉
You're all set!
We've got your details. Your Nostra swag is on its way. Keep making stores faster in the meantime.