Seven modules. Actionable in an afternoon.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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:
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
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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:
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.
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.
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).
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.
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.
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.
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:
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.
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.
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.
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.
310+ edge locations. AI-powered caching. No code changes. No developer resources on your end.
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.
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:
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.
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.
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 →Sixteen questions covering all seven modules. Click an answer to see if you got it right (and why).
Get a perfect score to claim a free Nostra hoodie or water bottle. We'll ship it anywhere.