- How To Properly Load Images from Shopify CDN in Next.js
How To Properly Load Images from Shopify CDN in Next.js
Avoid Vercel image transformation, eliminate 402 errors, and improve performance.

Here's the corrected markdown for your article, with improved formatting for code blocks, headings, and links:
When I started building headless e-commerce applications with Next.js and Shopify, I thought handling images would be straightforward. Just use Next.js Image component and everything would be optimized automatically, right? Wrong. I quickly learned that when dealing with Shopify images, there are important caveats that can cost you money and hurt performance.
Here's what I discovered: Shopify already provides a powerful CDN natively with every subscription. This CDN smartly creates files in multiple sizes and compresses them automatically. That means we don't need Vercel's image transformation service at all. In fact, using it creates problems.
The fun fact that caught me off guard is that whenever you use next/image
for a remote image, Vercel will try to optimize it again. This costs money beyond the 5,000 free transformations per month on hobby projects. Even worse, you'll start seeing 402 Payment Required errors in production when you hit those limits.
The Problem
I learned this the hard way when my production app started throwing 402 errors. Using Next.js <Image>
component with Shopify images without proper configuration causes several issues. First, you get double optimization where Vercel tries to re-optimize already optimized Shopify images. Second, you'll hit 402 Payment Required errors in production when exceeding Vercel's image optimization limits. Finally, this approach leads to unnecessary costs and slower performance since you're processing images twice instead of leveraging Shopify's already-optimized CDN.
The Solution: Next.js Image + Shopify CDN + unoptimized={true}
The solution I developed combines the best of both worlds. We use Next.js Image component to get all its benefits like lazy loading, blur placeholders, and error handling, but we tell it not to process the images with unoptimized={true}
. This way, we leverage Shopify's CDN directly while keeping all of Next.js's image features.
This combination works because Shopify's CDN already handles the heavy lifting of image optimization, compression, and multiple format generation. By adding unoptimized={true}
, we prevent Vercel from trying to re-process these already-optimized images, eliminating costs and improving performance.
Helper Functions
// Helper function to generate Shopify image URLs with size parameters
const getShopifyImageUrl = (url: string, width: number, height?: number): string => {
if (!url || url === PLACEHOLDER_IMAGE_URL) return url;
const separator = url.includes("?") ? "&" : "?";
let params = `width=${width}`;
if (height) {
params += `&height=${height}`;
}
return `${url}${separator}${params}`;
};
// Generate srcset for Shopify images
const getShopifyImageSrcSet = (url: string, widths: number[]): string => {
if (!url || url === PLACEHOLDER_IMAGE_URL) return "";
return widths
.map((width) => `${getShopifyImageUrl(url, width)} ${width}w`)
.join(", ");
};
The srcset
attribute is crucial for responsive images because it tells the browser which image size to download based on the device's screen density and viewport width. When you provide multiple image URLs with different widths in the srcset, the browser can make intelligent decisions about which image to fetch. This results in faster loading times on mobile devices (which download smaller images) and crisp images on high-density displays (which can download larger images when needed).
Implementation Example
// ❌ DON'T: Use Next.js Image without unoptimized
import Image from "next/image";
<Image
src={shopifyImage.url}
width={800}
height={600}
alt={shopifyImage.altText}
/>;
// ✅ DO: Use Next.js Image with unoptimized={true} and Shopify CDN
import Image from "next/image";
<Image
src={getShopifyImageUrl(shopifyImage.url, 1200)}
alt={shopifyImage.altText}
width={shopifyImage.width || 1200}
height={shopifyImage.height || 1200}
className="w-full h-auto object-contain"
priority={true} // for above-the-fold images
unoptimized={true} // Critical: prevents Vercel optimization
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q=="
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 50vw, (max-width: 1280px) 40vw, 33vw"
onLoad={() => setImageLoaded(true)}
onError={() => handleImageError(shopifyImage.url)}
/>;
You'll notice I'm using a hardcoded blurDataURL
. This is necessary because Next.js requires a data URL when you use placeholder="blur"
, but unfortunately Shopify doesn't provide blur data URLs. Rather than generate them dynamically (which would add complexity), I use a simple 1x1 pixel JPEG that creates a subtle blur effect during image loading.
Responsive Sizes with Tailwind Breakpoints
Understanding the logic behind the sizes
attribute is crucial for optimal performance. The sizes
string tells the browser how much space the image will occupy at different viewport widths.
// Main product images
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 50vw, (max-width: 1280px) 40vw, 33vw";
// Thumbnail images
sizes="(max-width: 640px) 25vw, (max-width: 768px) 20vw, (max-width: 1024px) 16.67vw, (max-width: 1280px) 14vw, 12vw";
For main product images, I specify that on mobile devices (up to 640px), the image takes up the full viewport width (100vw). On tablets and small desktops, it takes up half the viewport (50vw). As screens get larger, the image takes up proportionally less space. This approach ensures users download appropriately sized images for their devices.
Shopify Image URL Parameters
Shopify CDN supports these parameters that you can append to any image URL:
width=800
sets the image widthheight=600
sets the image height (optional)crop=center
defines crop position (center, top, bottom, left, right)format=webp
specifies output format (webp, jpg, png)
Benefits of This Approach
This method provides several key advantages. First, you avoid Vercel image optimization costs entirely since unoptimized={true}
bypasses Vercel's processing. Second, you still get all Next.js features including built-in lazy loading, blur placeholders, and error handling. Third, you achieve better performance by leveraging Shopify's global CDN combined with Next.js optimizations. Fourth, you eliminate 402 errors in production completely. Fifth, you maintain full TypeScript support with the Image component. Sixth, you get built-in loading states through onLoad
and onError
callbacks. Finally, you ensure proper accessibility with alt text and ARIA support.
Advanced Features
Loading States & Error Handling
To make the user experience even more seamless, I implement comprehensive loading and error states. This approach tracks which images have loaded successfully and which have failed, providing fallbacks for broken images.
const [imageLoaded, setImageLoaded] = useState(false);
const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
const handleImageError = (imageUrl: string) => {
setFailedImages((prev) => new Set([...prev, imageUrl]));
};
const getImageSrc = (imageUrl: string, width: number): string => {
const shopifyUrl = getShopifyImageUrl(imageUrl, width);
if (failedImages.has(shopifyUrl)) {
return PLACEHOLDER_IMAGE_URL;
}
return shopifyUrl;
};
The loading state management allows you to show skeleton loaders or loading animations while images are being fetched. The error handling ensures that if an image fails to load (perhaps due to a broken URL or network issues), you can automatically fall back to a placeholder image rather than showing a broken image icon.
Smart Preloading
For smooth navigation between images in galleries or carousels, I implement smart preloading. This technique preloads adjacent images before users navigate to them, creating a seamless browsing experience.
// Preload adjacent images for smooth navigation
const preloadImage = (imageUrl: string, width: number): void => {
const link = document.createElement("link");
link.rel = "preload";
link.as = "image";
link.href = getShopifyImageUrl(imageUrl, width);
document.head.appendChild(link);
};
This preloading strategy works by creating link elements with rel="preload"
that tell the browser to fetch images in the background. When users navigate to the next image in a product gallery, it's already cached and displays instantly. I typically preload the next and previous images relative to the currently viewed image to balance performance with memory usage.
Implementation Tips
When implementing this approach in your own projects, always use unoptimized={true}
to prevent double optimization. Use placeholder="blur"
with the hardcoded 1x1 pixel blur data URL for better user experience during loading. Set priority={true}
for above-the-fold images to ensure they load immediately. Use loading="lazy"
for images below the fold to improve initial page load performance. Implement proper error handling with fallback to placeholder images to handle broken URLs gracefully. Finally, use intersection observers for thumbnail lazy loading to only load images when they're about to come into view.
File References
See the complete implementation example in components/product/product-media-section.tsx which demonstrates all these concepts in a production-ready product gallery component.
Conclusion
Through building several headless commerce applications, I've learned that the key to optimal image performance with Shopify lies in understanding what each platform does best. Shopify excels at image optimization and global delivery through its CDN, while Next.js excels at progressive loading, user experience features, and developer experience.
By combining Next.js Image component with unoptimized={true}
and Shopify's native URL parameters, we get the best of both worlds without the costs and complexity of double optimization. This approach has saved me hundreds of dollars in Vercel image optimization fees while delivering better performance to users.
The techniques I've shared here - from smart preloading to error handling to responsive sizing - form the foundation of a robust image system that scales with your e-commerce needs. As you implement these patterns, you'll find that images become one less thing to worry about in your headless commerce stack.
Thanks, Matija