Lazy loading defers the download of images, videos, and scripts until a visitor actually needs them. On a Shopify collection page with 24 products, lazy loading can cut the initial page weight by 60 to 70 percent, loading only what is visible and fetching the rest as the visitor scrolls. Get the implementation right - native browser lazy loading for images, Intersection Observer for scripts and sections - and your PageSpeed scores improve immediately without removing a single piece of content.
What Is Lazy Loading and Why Shopify Stores Need It
When a browser loads a web page, its default behavior is to download everything referenced in the HTML: every image, every script, every stylesheet, every embedded widget. For a content-rich Shopify store, this means a visitor loading your collection page downloads 24 product images, 3 section background images, a review widget, a wishlist app, and a currency converter - all before they have scrolled past the first row of products.
Lazy loading changes this default. Instead of loading everything at once, the browser loads only what is visible in the current viewport. Resources below the fold are queued and downloaded as the visitor scrolls toward them.
Themes are built with multiple sections, featured collections, trust badges, and review areas that push significant content below the fold on every page type. Without lazy loading, all of it downloads before the visitor sees the second row of products.
Each app loads resources on page load by default. Without lazy loading, a store with 10 apps loads all 10 apps' resources before the visitor sees the second row of products.
On a throttled 4G connection, downloading 2.4MB upfront instead of 400KB is the difference between a 3-second load and a 9-second load. Lazy loading is not optional for mobile-first stores.
What to Lazy Load in Shopify
Not everything should be lazy loaded. Getting the scope right is as important as the implementation itself.
- Product images in collection grids (except the first 4 to 8)
- Gallery images on product pages (except the first)
- Section background images below the fold
- Video embeds (YouTube, Vimeo, MP4 background videos)
- Review carousels, Instagram feeds, recommendation sections
- Chat widgets, heatmap tools, social proof notifications
- Your hero image or main product image (LCP elements)
- The first row of product images on a collection page
- Navigation and header assets
- Scripts that above-the-fold interactions depend on
- Fonts used in above-the-fold text (causes CLS)
Native Browser Lazy Loading vs JavaScript Lazy Loading
There are two ways to implement lazy loading in Shopify: the native browser loading="lazy" attribute and JavaScript-based implementations using Intersection Observer. Understanding when to use each determines the quality of your implementation.
loading="lazy"
Built into modern browsers. No JavaScript required. No library to load. No extra weight added to the page. Works at the browser rendering engine level, which is faster and more efficient than any JavaScript implementation. Browser support exceeds 95 percent globally.
The browser determines what counts as "near the viewport" itself. The threshold varies by browser and network speed but is typically 1 to 3 screen heights below the current scroll position. Content loads before the visitor reaches it, so there is rarely a visible delay.
A browser API that fires a callback when an observed element enters or exits the viewport. Runs off the main thread, meaning it does not add to JavaScript execution time the way scroll event listeners do.
Use when you need to lazy load entire sections of content, defer script initialization until a section is visible, set custom thresholds, or handle videos and app widgets that the loading attribute does not cover.
loading="lazy" for all images and iframes below the fold. Intersection Observer for app scripts, section initializations, and video embeds. They are not mutually exclusive and work better in combination than either does alone.How to Lazy Load Images in Shopify
Implementing lazy loading for images in Shopify Liquid is direct. The key is combining the loading attribute with correct dimension attributes to prevent CLS.
For product images in collection grids:
{% assign eager_count = 4 %}
{% for product in collection.products %}
{% assign lazy = forloop.index > eager_count %}
<img
src="{{ product.featured_image | image_url: width: 400, format: 'webp' }}"
loading="{{ lazy | ternary: 'lazy', 'eager' }}"
width="{{ product.featured_image.width }}"
height="{{ product.featured_image.height }}"
alt="{{ product.featured_image.alt | escape }}"
>
{% endfor %}
eager_count based on your grid columns. A 3-column grid on desktop should use eager_count = 6 to cover two rows. A 4-column grid should use eager_count = 8. The goal is to cover everything visible above the fold on the first load.For product gallery images:
{% for image in product.images %}
<img
src="{{ image | image_url: width: 800, format: 'webp' }}"
loading="{{ forloop.first | ternary: 'eager', 'lazy' }}"
fetchpriority="{{ forloop.first | ternary: 'high', 'auto' }}"
width="{{ image.width }}"
height="{{ image.height }}"
alt="{{ image.alt | escape }}"
>
{% endfor %}
The first image gets fetchpriority="high" to ensure it loads as the LCP element. All subsequent images are lazy loaded.
For section background images:
{% if section.settings.background_image %}
<img
src="{{ section.settings.background_image | image_url: width: 1400, format: 'webp' }}"
loading="lazy"
width="1400"
height="600"
alt="{{ section.settings.background_image.alt | escape }}"
class="section-bg-image"
>
{% endif %}
width="{{ image.width }}" height="{{ image.height }}"
How to Lazy Load Videos in Shopify
Videos are the heaviest assets on most Shopify homepages. A background video that autoloads on page open adds several megabytes to the initial page weight. Lazy loading video requires a different approach than images.
Use a poster image (a static frame from the video) as a visible placeholder. Load the video source only when the container enters the viewport using Intersection Observer. Visitors see the poster image instantly. The video loads when the container enters the viewport.
Never embed YouTube or Vimeo with a standard <iframe> if performance matters. Each embed loads several hundred kilobytes of JavaScript before the video plays. Use a facade pattern instead: show a thumbnail image with a play button overlay, and only load the actual iframe when the visitor clicks play. This saves 400 to 600KB of JavaScript per embed.
YouTube facade pattern implementation:
<div class="video-facade" data-video-id="YOUR_VIDEO_ID">
<img
src="https://img.youtube.com/vi/YOUR_VIDEO_ID/maxresdefault.jpg"
loading="lazy"
width="1280"
height="720"
alt="Video thumbnail"
>
<button class="play-button" aria-label="Play video">
<svg><!-- play icon --></svg>
</button>
</div>
document.querySelectorAll('.video-facade').forEach(facade => {
facade.querySelector('.play-button').addEventListener('click', function() {
const videoId = facade.dataset.videoId;
const iframe = document.createElement('iframe');
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
iframe.allow = 'autoplay; encrypted-media';
iframe.allowFullscreen = true;
facade.replaceWith(iframe);
});
});
How to Lazy Load Shopify Sections
Lazy loading entire sections means deferring the script initialization and heavy content rendering for below-the-fold sections until the visitor is near them. This is more complex than image lazy loading but delivers significant gains for content-heavy homepages.
document.querySelectorAll('[data-lazy-section]').forEach(section => {
const observer = new IntersectionObserver(function(entries) {
if (entries[0].isIntersecting) {
const sectionType = section.dataset.lazySection;
initSection(sectionType, section);
observer.unobserve(section);
}
}, { rootMargin: '400px 0px' }); // Start loading 400px before visible
observer.observe(section);
});
function initSection(type, element) {
switch(type) {
case 'reviews': loadReviewWidget(element); break;
case 'instagram': loadInstagramFeed(element); break;
case 'recommendations': loadProductRecommendations(element); break;
}
}
In your Liquid section files, add the data-lazy-section attribute and a placeholder to prevent layout shift:
<div data-lazy-section="reviews" class="reviews-section">
<div class="reviews-placeholder">
<div class="placeholder-line"></div>
<div class="placeholder-line"></div>
</div>
</div>
Avoiding SEO Issues with Lazy Loading
Lazy loading done incorrectly can hide content from Google's crawler, which affects indexing and rankings. Done correctly, lazy loading has no negative SEO impact.
- Images using
loading="lazy"- Googlebot crawls these - Content loaded via Intersection Observer on scroll
- Lazy-loaded images with descriptive alt text
- Server-side Liquid rendered text content
- Lazy-loaded images without alt text
- Product names and descriptions only appearing after JS initialization
- Content that only loads after a button click (not scroll)
- Critical page text hidden behind lazy loading
Fixing Common Lazy Loading Bugs in Shopify
Fix: Add a background color or skeleton placeholder to image containers so the load feels progressive rather than broken:
.product-image-container {
background-color: #f0f0f0;
aspect-ratio: 4 / 5;
}
Fix: Always include width and height attributes. For Shopify images, use the object's built-in properties: width="{{ image.width }}" height="{{ image.height }}". This lets the browser reserve the correct space before the image downloads.
Fix: Audit every <img> tag in your product and homepage templates. Confirm the first visible image uses loading="eager" and fetchpriority="high". Search theme files for loading="lazy" and verify none apply to the hero or main product image.
Fix: Add a fallback for older browsers that load all deferred content immediately:
if ('IntersectionObserver' in window) {
// Use observer
} else {
// Load all immediately
}
Performance Testing for Lazy Loading
Lazy loading improvements show up in specific metrics. Knowing which metrics to watch helps you validate that your implementation is working correctly.
In Chrome DevTools Network tab, look at the total transferred size at the bottom of the panel after the initial page load (before any scrolling). This should decrease significantly after implementing lazy loading. The difference represents resources deferred to load-on-scroll.
LCP should improve because the browser is no longer competing to download off-screen images simultaneously with the LCP element. Fewer concurrent downloads means more bandwidth available for the LCP image.
Deferring heavy app scripts and section initializations reduces the JavaScript execution burden during the critical load window, typically improving Time to Interactive by 0.5 to 2 seconds on stores with many apps.
Use WebPageTest.org with filmstrip view. This shows a frame-by-frame screenshot of the page loading. With lazy loading enabled, you will see above-the-fold content appear quickly while below-the-fold content loads progressively as the simulated scroll continues.
Advanced Lazy Loading Techniques for Shopify
The fetchpriority attribute allows you to hint to the browser how important each resource is, beyond just eager vs lazy. The first image loads with high priority (LCP). Images 2 to 4 load eagerly but with low priority so they do not compete with the first image. Everything else loads lazily.
Skeleton screens replace blank placeholders with an animated loading state that matches the shape of the incoming content. This is the pattern used by major ecommerce platforms. It signals to visitors that content is coming rather than that the page is broken, reducing bounce rate in the fraction of a second between scroll and load.
Instead of waiting for elements to enter the viewport, use scroll velocity to predict where the visitor is heading and preload content slightly ahead. This is a refinement rather than a foundational fix, appropriate after the core lazy loading implementation is in place.
Skeleton screen CSS implementation:
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Shopify-Specific Lazy Loading Tips
Images using Shopify's CDN filter support format conversion and responsive resizing. A lazy-loaded image that is still a 2MB JPEG defeats the purpose. Every lazy-loaded image should use image_url with a width parameter and format: 'webp'.
Shopify has distinct page templates: index, product, collection, article, page, cart. Your lazy loading implementation may work on collection pages but have edge cases on article or search result pages. Test each template separately.
Some Shopify apps initialize immediately and modify image elements, removing lazy loading attributes in the process. After implementing lazy loading, inspect your product images in DevTools to confirm the loading="lazy" attribute is still present on the rendered HTML.
Best Practices Checklist
Image Lazy Loading
- Load the first visible row of images eagerly
- Use
loading="lazy"as the default for all below-fold images - Always pair with explicit
widthandheightattributes - Use
fetchpriority="high"on the LCP image - Use
image_urlwith WebP format on every image
Scripts and Sections
- Use Intersection Observer for scripts, video, and section initializations
- Set
rootMargin: '300px'to preload before the visitor arrives - Add placeholder content to prevent layout shift
- Use YouTube facade pattern for video embeds
- Add Intersection Observer fallback for older browsers
Testing and Maintenance
- Test in PageSpeed Insights and DevTools Network tab
- Check Google Search Console to confirm lazy-loaded content is indexed
- Retest after every theme update and app installation
- Inspect rendered HTML to confirm lazy attributes survive app initialization
- Use WebPageTest filmstrip view to validate progressive loading
Summary
Lazy loading is one of the highest return-on-effort optimizations available for Shopify stores. The implementation is straightforward, the browser support is excellent, and the performance gains are immediate and measurable.
loading="lazy" for images and iframes. Use Intersection Observer for scripts, videos, and section initializations. Never lazy load above-the-fold content. Always include image dimensions. Test in DevTools to confirm resources are actually being deferred, and check Search Console to confirm Google is still indexing your content.Pair lazy loading with WebP image compression and correct image sizing - available through tools like Ecom: Page Speed Expert - and the compound effect on your PageSpeed scores and real user experience is significant. Each optimization layer reinforces the others. Lazy loading is the one that makes everything else more effective.