SuiteCommerce Image Optimization: A Developer's Guide
Images account for 75% of total page weight on the average SuiteCommerce store. They're also the single largest contributor to poor Largest Contentful Paint (LCP) scores—the Core Web Vital that Google uses to evaluate loading performance. After auditing hundreds of SuiteCommerce sites, we've found that image optimization alone can reduce page weight by 40-60% and cut LCP times in half.
This isn't about vague best practices. This guide gives you specific implementation details for WebP conversion, lazy loading, responsive images, CDN configuration, and building custom image optimization extensions. Every technique includes code examples you can deploy today.
Table of Contents
- Why Images Matter for SuiteCommerce Performance
- Image Format Selection: WebP, AVIF, and Fallbacks
- Implementing Lazy Loading in SuiteCommerce
- Responsive Images for SuiteCommerce Themes
- CDN Configuration for Images
- Optimizing Product Images at Upload
- Building an Image Optimization Extension
- Measuring Image Performance Impact
- Common Pitfalls and How to Avoid Them
- FAQ
Why Images Matter for SuiteCommerce Performance
Let's put numbers to the problem.
A typical SuiteCommerce product listing page loads 20-40 product images. Without optimization, each image averages 200-500KB. That's 4-20MB of images per page—before any other assets load.
The Performance Impact
| Metric | Unoptimized | Optimized | Improvement |
|---|---|---|---|
| Product listing page weight | 8.2MB | 2.1MB | 74% reduction |
| Time to LCP | 4.8s | 1.9s | 60% faster |
| Mobile load time (3G) | 18s+ | 6s | 66% faster |
| Image requests | 45 | 12 (above fold) | 73% fewer initial |
Those aren't theoretical numbers. They're from a recent SuiteCommerce optimization project where images were the primary bottleneck.
The Business Impact
Google's research shows that a 100ms delay in load time reduces conversion rates by 7%. When your competitor's page loads in 2 seconds and yours takes 5 seconds, you're losing sales—not because of your products, but because of your image delivery.
Mobile users are even less patient. 53% of mobile users abandon sites that take longer than 3 seconds to load. If your images are pushing you past that threshold, you're sending traffic to competitors.
How SuiteCommerce Handles Images by Default
Out of the box, SuiteCommerce:
- Serves images in their original upload format (usually JPEG or PNG)
- Doesn't implement lazy loading
- Doesn't generate responsive image variants
- Relies on basic browser caching with limited control
This default configuration was acceptable in 2018. It's a serious liability in 2025.
Image Format Selection: WebP, AVIF, and Fallbacks
Modern image formats deliver the same visual quality at 30-50% smaller file sizes. Supporting them requires format detection and fallback logic.
WebP: The Current Standard
WebP provides 25-35% smaller file sizes than JPEG at equivalent quality. Browser support is now universal—97%+ of global users can view WebP images natively.
Converting images to WebP:
If you're managing product images through NetSuite's File Cabinet, convert images before upload:
# Using cwebp (Google's WebP converter)
# Install: brew install webp (macOS) or apt install webp (Linux)
# Convert single image
cwebp -q 80 input.jpg -o output.webp
# Batch convert directory
for file in *.jpg; do
cwebp -q 80 "$file" -o "${file%.jpg}.webp"
done
For quality settings:
- Product images: Quality 80-85 (balances file size and detail)
- Lifestyle/hero images: Quality 85-90 (preserves more detail)
- Thumbnails: Quality 70-75 (detail matters less at small sizes)
AVIF: The Next Generation
AVIF offers 20% additional compression over WebP with better color depth. Browser support is at 92% and growing. The catch: encoding is significantly slower.
# Using avifenc
avifenc --min 20 --max 40 --speed 4 input.jpg output.avif
# Batch convert
for file in *.jpg; do
avifenc --min 20 --max 40 --speed 4 "$file" "${file%.jpg}.avif"
done
Implementing Format Fallbacks in SuiteCommerce
The <picture> element lets you serve modern formats with fallbacks:
{{!-- product_image.tpl --}}
<picture class="product-image-container">
{{#if hasAvif}}
<source
srcset="{{avifUrl}}"
type="image/avif"
/>
{{/if}}
{{#if hasWebp}}
<source
srcset="{{webpUrl}}"
type="image/webp"
/>
{{/if}}
<img
src="{{imageUrl}}"
alt="{{altText}}"
class="product-image"
loading="lazy"
width="{{width}}"
height="{{height}}"
/>
</picture>
Creating a View helper for format detection:
// ImageFormatHelper.js
define('ImageFormatHelper', [
'underscore'
], function(_) {
'use strict';
var formatSuffixes = {
webp: '.webp',
avif: '.avif'
};
return {
/**
* Generate URLs for multiple image formats
* @param {string} baseUrl - Original image URL
* @returns {Object} URLs for different formats
*/
getFormatUrls: function(baseUrl) {
if (!baseUrl) {
return {};
}
// Extract base path without extension
var lastDot = baseUrl.lastIndexOf('.');
var basePath = baseUrl.substring(0, lastDot);
var originalExt = baseUrl.substring(lastDot);
return {
original: baseUrl,
webp: basePath + formatSuffixes.webp,
avif: basePath + formatSuffixes.avif,
hasWebp: this.webpExists(basePath + formatSuffixes.webp),
hasAvif: this.avifExists(basePath + formatSuffixes.avif)
};
},
/**
* Check if WebP version exists
* In practice, this checks your CDN/file naming convention
*/
webpExists: function(url) {
// If you're auto-generating WebP, assume it exists
// For explicit file checking, implement server-side validation
return true;
},
avifExists: function(url) {
// AVIF support may be selective
return false; // Enable when AVIF generation is configured
}
};
});
Server-Side Format Selection
For dynamic format selection, modify your image service controller:
// ImageService.ServiceController.js
define('ImageService.ServiceController', [
'ServiceController'
], function(ServiceController) {
'use strict';
return ServiceController.extend({
name: 'ImageService.ServiceController',
get: function() {
var imageId = this.request.getParameter('id');
var requestedFormat = this.request.getParameter('format');
var accept = this.request.getHeader('Accept') || '';
// Auto-detect best format from Accept header
var format = requestedFormat || this.detectBestFormat(accept);
var imageUrl = this.getImageUrl(imageId, format);
// Redirect to optimized image
this.response.setHeader('Cache-Control', 'public, max-age=31536000');
this.response.setHeader('Vary', 'Accept');
return {
url: imageUrl,
format: format
};
},
detectBestFormat: function(acceptHeader) {
if (acceptHeader.indexOf('image/avif') !== -1) {
return 'avif';
}
if (acceptHeader.indexOf('image/webp') !== -1) {
return 'webp';
}
return 'jpeg';
},
getImageUrl: function(imageId, format) {
// Implementation depends on your image storage
var baseUrl = this.getBaseImageUrl(imageId);
switch (format) {
case 'avif':
return baseUrl.replace(/\.(jpg|jpeg|png)$/i, '.avif');
case 'webp':
return baseUrl.replace(/\.(jpg|jpeg|png)$/i, '.webp');
default:
return baseUrl;
}
}
});
});
Implementing Lazy Loading in SuiteCommerce
Lazy loading defers image loading until images enter the viewport. This dramatically reduces initial page weight and speeds up First Contentful Paint.
Native Lazy Loading
The simplest implementation uses the browser's native lazy loading:
{{!-- product_thumbnail.tpl --}}
<img
src="{{imageUrl}}"
alt="{{productName}}"
loading="lazy"
width="{{width}}"
height="{{height}}"
class="product-thumbnail"
/>
Critical consideration: Always include width and height attributes. Without them, the browser can't reserve space before the image loads, causing Cumulative Layout Shift (CLS)—another Core Web Vital that hurts your Google ranking.
Advanced Lazy Loading with Intersection Observer
For more control, implement custom lazy loading:
// LazyImageLoader.js
define('LazyImageLoader', [
'jQuery',
'underscore'
], function($, _) {
'use strict';
var LazyImageLoader = function(options) {
this.options = _.extend({
rootMargin: '50px 0px',
threshold: 0.01,
selector: '[data-lazy-src]'
}, options);
this.observer = null;
this.initialize();
};
_.extend(LazyImageLoader.prototype, {
initialize: function() {
if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: this.options.rootMargin,
threshold: this.options.threshold
}
);
this.observeImages();
} else {
// Fallback for older browsers
this.loadAllImages();
}
},
observeImages: function() {
var self = this;
var images = document.querySelectorAll(this.options.selector);
images.forEach(function(img) {
self.observer.observe(img);
});
},
handleIntersection: function(entries) {
var self = this;
entries.forEach(function(entry) {
if (entry.isIntersecting) {
self.loadImage(entry.target);
self.observer.unobserve(entry.target);
}
});
},
loadImage: function(img) {
var src = img.getAttribute('data-lazy-src');
var srcset = img.getAttribute('data-lazy-srcset');
if (src) {
img.src = src;
img.removeAttribute('data-lazy-src');
}
if (srcset) {
img.srcset = srcset;
img.removeAttribute('data-lazy-srcset');
}
// Add loaded class for CSS transitions
img.classList.add('lazy-loaded');
},
loadAllImages: function() {
var self = this;
var images = document.querySelectorAll(this.options.selector);
images.forEach(function(img) {
self.loadImage(img);
});
},
// Call when new images are added (e.g., infinite scroll)
refresh: function() {
this.observeImages();
},
destroy: function() {
if (this.observer) {
this.observer.disconnect();
}
}
});
return LazyImageLoader;
});
Template modification for custom lazy loading:
{{!-- product_list_item.tpl --}}
<div class="product-list-item">
<img
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3C/svg%3E"
data-lazy-src="{{imageUrl}}"
data-lazy-srcset="{{imageSrcset}}"
alt="{{productName}}"
width="400"
height="400"
class="product-list-item-image lazy-image"
/>
</div>
CSS for smooth loading transitions:
// _lazy-images.scss
.lazy-image {
opacity: 0;
transition: opacity 0.3s ease-in-out;
background-color: #f5f5f5;
&.lazy-loaded {
opacity: 1;
}
}
// Placeholder styling
.lazy-image[src^="data:"] {
background: linear-gradient(90deg, #f5f5f5 25%, #ececec 50%, #f5f5f5 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
Eager Loading for Above-the-Fold Images
Don't lazy load images that appear immediately on page load. Use loading="eager" or omit the attribute entirely:
{{!-- hero_image.tpl --}}
<img
src="{{heroImageUrl}}"
alt="{{heroAltText}}"
loading="eager"
fetchpriority="high"
class="hero-image"
/>
The fetchpriority="high" attribute tells the browser to prioritize this image, improving LCP.
Rule of thumb: Eager load the first 1-3 images visible without scrolling. Lazy load everything else.
Responsive Images for SuiteCommerce Themes
Serving appropriately-sized images prevents mobile users from downloading desktop-sized images.

Understanding srcset and sizes
The srcset attribute provides multiple image sizes. The sizes attribute tells the browser which size to choose:
<img
srcset="
product-400w.webp 400w,
product-600w.webp 600w,
product-800w.webp 800w,
product-1200w.webp 1200w
"
sizes="
(max-width: 480px) 100vw,
(max-width: 768px) 50vw,
(max-width: 1200px) 33vw,
400px
"
src="product-800w.webp"
alt="Product Name"
/>
This tells the browser:
- On phones (≤480px): The image fills the viewport width
- On tablets (≤768px): The image is half the viewport width
- On desktop (≤1200px): The image is one-third the viewport width
- Larger screens: The image is 400px wide
Implementing Responsive Images in SuiteCommerce
Create a responsive image helper:
// ResponsiveImageHelper.js
define('ResponsiveImageHelper', [
'underscore',
'SC.Configuration'
], function(_, Configuration) {
'use strict';
var breakpoints = [400, 600, 800, 1200, 1600];
var defaultSizes = {
'product-listing': '(max-width: 480px) 100vw, (max-width: 768px) 50vw, (max-width: 1200px) 33vw, 400px',
'product-detail': '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px',
'thumbnail': '100px',
'hero': '100vw'
};
return {
/**
* Generate srcset from base image URL
* Assumes image resizing service is available
*/
generateSrcset: function(baseUrl, maxWidth) {
if (!baseUrl) {
return '';
}
var applicableBreakpoints = breakpoints.filter(function(bp) {
return !maxWidth || bp <= maxWidth;
});
return applicableBreakpoints.map(function(width) {
var resizedUrl = this.getResizedUrl(baseUrl, width);
return resizedUrl + ' ' + width + 'w';
}.bind(this)).join(', ');
},
/**
* Get the sizes attribute for a given context
*/
getSizes: function(context) {
return defaultSizes[context] || defaultSizes['product-listing'];
},
/**
* Transform URL to point to resized version
* Implementation depends on your image service
*/
getResizedUrl: function(originalUrl, width) {
// Option 1: URL parameter (if using image processing service)
// return originalUrl + '?w=' + width + '&format=webp';
// Option 2: URL path modification (pre-generated sizes)
var lastDot = originalUrl.lastIndexOf('.');
var basePath = originalUrl.substring(0, lastDot);
var ext = '.webp'; // Always serve WebP in srcset
return basePath + '-' + width + 'w' + ext;
},
/**
* Get complete responsive image attributes
*/
getImageAttributes: function(imageUrl, context, options) {
options = options || {};
return {
src: this.getResizedUrl(imageUrl, options.defaultWidth || 800),
srcset: this.generateSrcset(imageUrl, options.maxWidth),
sizes: this.getSizes(context),
width: options.width || '',
height: options.height || ''
};
}
};
});
Using the helper in a View:
// ProductListItem.View.js
define('ProductListItem.View', [
'Backbone',
'ResponsiveImageHelper',
'product_list_item.tpl'
], function(Backbone, ResponsiveImageHelper, template) {
'use strict';
return Backbone.View.extend({
template: template,
getContext: function() {
var imageUrl = this.model.get('thumbnail');
var imageAttrs = ResponsiveImageHelper.getImageAttributes(
imageUrl,
'product-listing',
{
defaultWidth: 400,
maxWidth: 800,
width: 400,
height: 400
}
);
return {
productName: this.model.get('displayname'),
productUrl: this.model.get('url'),
imageUrl: imageAttrs.src,
imageSrcset: imageAttrs.srcset,
imageSizes: imageAttrs.sizes,
imageWidth: imageAttrs.width,
imageHeight: imageAttrs.height
};
}
});
});
Template using responsive image attributes:
{{!-- product_list_item.tpl --}}
<a href="{{productUrl}}" class="product-list-item-link">
<img
src="{{imageUrl}}"
srcset="{{imageSrcset}}"
sizes="{{imageSizes}}"
alt="{{productName}}"
width="{{imageWidth}}"
height="{{imageHeight}}"
loading="lazy"
class="product-list-item-image"
/>
<span class="product-list-item-name">{{productName}}</span>
</a>
Generating Multiple Image Sizes
For pre-generated responsive images, create a build script:
// scripts/generate-responsive-images.js
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const sizes = [400, 600, 800, 1200, 1600];
const inputDir = './images/originals';
const outputDir = './images/responsive';
async function processImage(inputPath) {
const filename = path.basename(inputPath, path.extname(inputPath));
for (const width of sizes) {
const outputPath = path.join(outputDir, `${filename}-${width}w.webp`);
await sharp(inputPath)
.resize(width, null, {
fit: 'inside',
withoutEnlargement: true
})
.webp({ quality: 80 })
.toFile(outputPath);
console.log(`Generated: ${outputPath}`);
}
}
async function processDirectory() {
const files = fs.readdirSync(inputDir);
for (const file of files) {
if (/\.(jpg|jpeg|png)$/i.test(file)) {
await processImage(path.join(inputDir, file));
}
}
}
processDirectory().catch(console.error);
CDN Configuration for Images
A properly configured CDN reduces image delivery time by 50-80%.
CDN Selection for SuiteCommerce
SuiteCommerce sites commonly use:
- Akamai (NetSuite's default CDN partner)
- Cloudflare (cost-effective, excellent performance)
- AWS CloudFront (flexible, good for custom setups)
- Fastly (premium performance, higher cost)
Each CDN supports image optimization features. Configure them to maximize benefit.
Cloudflare Image Optimization
If using Cloudflare, enable these features:
Polish (image compression):
Dashboard → Speed → Optimization → Image Optimization → Polish: Lossless
WebP conversion:
Dashboard → Speed → Optimization → Image Optimization → WebP: On
Mirage (lazy loading for mobile):
Dashboard → Speed → Optimization → Image Optimization → Mirage: On
Cache rules for images:
# Page Rule
URL: *yourdomain.com/*.jpg*
Settings:
- Cache Level: Cache Everything
- Edge Cache TTL: 1 month
- Browser Cache TTL: 1 week
Custom CDN Headers
Set proper cache headers in your image service:
// ImageService.ServiceController.js
get: function() {
var imageUrl = this.request.getParameter('url');
// Set CDN-friendly cache headers
this.response.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
this.response.setHeader('Vary', 'Accept'); // Vary by Accept for format negotiation
// ETag for cache validation
var etag = this.generateETag(imageUrl);
this.response.setHeader('ETag', etag);
return { url: imageUrl };
}
CDN URL Rewriting
Rewrite image URLs to point to your CDN:
// CDNImageUrl.js
define('CDNImageUrl', [
'SC.Configuration'
], function(Configuration) {
'use strict';
var cdnBase = Configuration.get('images.cdnBaseUrl', '');
var enabled = Configuration.get('images.useCDN', false);
return {
transform: function(originalUrl) {
if (!enabled || !cdnBase || !originalUrl) {
return originalUrl;
}
// Replace NetSuite file cabinet URL with CDN URL
if (originalUrl.indexOf('netsuite.com') !== -1 ||
originalUrl.indexOf('/site/') !== -1) {
var pathMatch = originalUrl.match(/\/site\/(.+)$/);
if (pathMatch) {
return cdnBase + '/' + pathMatch[1];
}
}
return originalUrl;
}
};
});
Optimizing Product Images at Upload
The best optimization happens before images enter your system.
Image Upload Guidelines
Provide your team with clear specifications:
| Image Type | Max Dimensions | Max File Size | Format |
|---|---|---|---|
| Product main | 2000 x 2000px | 500KB | JPEG (q85) |
| Product alt views | 2000 x 2000px | 500KB | JPEG (q85) |
| Lifestyle/hero | 2400 x 1200px | 800KB | JPEG (q85) |
| Thumbnails | 400 x 400px | 50KB | JPEG (q80) |
| Icons/logos | 200 x 200px | 20KB | PNG-8 or SVG |
Automated Upload Processing
Create a SuiteScript that processes images on upload:
// ImageUploadProcessor.js
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/file', 'N/log'], function(file, log) {
function afterSubmit(context) {
if (context.type !== context.UserEventType.CREATE) {
return;
}
var newRecord = context.newRecord;
var itemId = newRecord.id;
// Get the uploaded image
var imageId = newRecord.getValue({ fieldId: 'custitem_main_image' });
if (!imageId) {
return;
}
try {
var imageFile = file.load({ id: imageId });
var dimensions = getImageDimensions(imageFile);
// Check if optimization needed
if (dimensions.width > 2000 || dimensions.height > 2000) {
log.warning({
title: 'Oversized Image',
details: 'Item ' + itemId + ' has oversized image: ' +
dimensions.width + 'x' + dimensions.height
});
// Option: Auto-resize or notify admin
notifyAdminOversizedImage(itemId, dimensions);
}
// Check file size
var sizeKB = imageFile.size / 1024;
if (sizeKB > 500) {
log.warning({
title: 'Large Image File',
details: 'Item ' + itemId + ' image is ' +
Math.round(sizeKB) + 'KB (limit: 500KB)'
});
}
} catch (e) {
log.error({
title: 'Image Processing Error',
details: e.message
});
}
}
function getImageDimensions(imageFile) {
// Implementation depends on image library availability
// NetSuite native doesn't include image dimension reading
// You may need to parse JPEG/PNG headers manually or use external service
return { width: 0, height: 0 };
}
function notifyAdminOversizedImage(itemId, dimensions) {
// Send email or create task for admin
}
return {
afterSubmit: afterSubmit
};
});
Bulk Image Optimization Script
For existing catalogs, create a scheduled script to optimize images:
// BulkImageOptimizer.js
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/search', 'N/record', 'N/file', 'N/log', 'N/runtime'],
function(search, record, file, log, runtime) {
function execute(context) {
var itemSearch = search.create({
type: search.Type.INVENTORY_ITEM,
filters: [
['isinactive', 'is', 'F'],
'AND',
['custitem_images_optimized', 'is', 'F']
],
columns: ['internalid', 'itemid', 'storedisplayimage']
});
var results = itemSearch.run().getRange({ start: 0, end: 100 });
results.forEach(function(result) {
var remainingUsage = runtime.getCurrentScript().getRemainingUsage();
if (remainingUsage < 100) {
log.audit({
title: 'Governance Limit',
details: 'Stopping to avoid governance limit'
});
return false;
}
try {
processItemImages(result.id);
// Mark as processed
record.submitFields({
type: record.Type.INVENTORY_ITEM,
id: result.id,
values: {
custitem_images_optimized: true
}
});
} catch (e) {
log.error({
title: 'Item Processing Error',
details: 'Item ' + result.id + ': ' + e.message
});
}
});
}
function processItemImages(itemId) {
// Implementation: resize, compress, convert to WebP
// This often requires external image processing service
log.audit({
title: 'Processing Item',
details: 'Item ID: ' + itemId
});
}
return {
execute: execute
};
});
Building an Image Optimization Extension
Combine all techniques into a reusable extension.
Extension Structure
ImageOptimization/
├── Modules/
│ └── ImageOptimization/
│ ├── JavaScript/
│ │ ├── ImageOptimization.js
│ │ ├── ImageOptimization.LazyLoader.js
│ │ ├── ImageOptimization.ResponsiveHelper.js
│ │ └── ImageOptimization.FormatHelper.js
│ ├── Templates/
│ │ └── optimized_image.tpl
│ ├── Sass/
│ │ └── _image-optimization.scss
│ └── Configuration/
│ └── ImageOptimization.json
├── ns.package.json
└── manifest.json
Main Module Entry Point
// ImageOptimization.js
define('ImageOptimization', [
'ImageOptimization.LazyLoader',
'ImageOptimization.ResponsiveHelper',
'ImageOptimization.FormatHelper',
'SC.Configuration'
], function(
LazyLoader,
ResponsiveHelper,
FormatHelper,
Configuration
) {
'use strict';
var config = Configuration.get('imageOptimization', {});
return {
LazyLoader: LazyLoader,
ResponsiveHelper: ResponsiveHelper,
FormatHelper: FormatHelper,
mountToApp: function(application) {
var self = this;
// Initialize lazy loading if enabled
if (config.lazyLoadingEnabled !== false) {
this.lazyLoader = new LazyLoader({
rootMargin: config.lazyLoadingMargin || '100px 0px',
selector: config.lazyLoadingSelector || '[data-lazy-src]'
});
}
// Extend image rendering in templates
Handlebars.registerHelper('optimizedImage', function(options) {
return self.renderOptimizedImage(options.hash);
});
// Listen for view renders to refresh lazy loading
application.getLayout().on('afterAppendView', function() {
if (self.lazyLoader) {
self.lazyLoader.refresh();
}
});
},
renderOptimizedImage: function(options) {
var url = options.url;
var context = options.context || 'product-listing';
var alt = options.alt || '';
var cssClass = options.class || '';
var lazy = options.lazy !== false;
var formatUrls = this.FormatHelper.getFormatUrls(url);
var responsiveAttrs = this.ResponsiveHelper.getImageAttributes(url, context, {
width: options.width,
height: options.height
});
var html = '<picture class="optimized-image ' + cssClass + '">';
// AVIF source (if available)
if (formatUrls.hasAvif) {
html += '<source srcset="' + formatUrls.avif + '" type="image/avif" />';
}
// WebP source
if (formatUrls.hasWebp) {
html += '<source srcset="' + formatUrls.webp + '" type="image/webp" />';
}
// Fallback img
html += '<img ';
if (lazy) {
html += 'src="data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 ' +
(options.width || 400) + ' ' + (options.height || 400) + '\'%3E%3C/svg%3E" ';
html += 'data-lazy-src="' + responsiveAttrs.src + '" ';
html += 'data-lazy-srcset="' + responsiveAttrs.srcset + '" ';
} else {
html += 'src="' + responsiveAttrs.src + '" ';
html += 'srcset="' + responsiveAttrs.srcset + '" ';
}
html += 'sizes="' + responsiveAttrs.sizes + '" ';
html += 'alt="' + alt + '" ';
html += 'width="' + (options.width || '') + '" ';
html += 'height="' + (options.height || '') + '" ';
html += 'class="optimized-image-img' + (lazy ? ' lazy-image' : '') + '" ';
html += '/></picture>';
return new Handlebars.SafeString(html);
}
};
});
Using the Extension in Templates
{{!-- product_list_item.tpl --}}
<div class="product-list-item">
{{optimizedImage
url=imageUrl
alt=productName
context="product-listing"
width=400
height=400
lazy=true
class="product-list-item-image"
}}
<h3 class="product-list-item-name">{{productName}}</h3>
</div>
Measuring Image Performance Impact
You can't improve what you don't measure.

Key Metrics to Track
| Metric | Target | Measurement Tool |
|---|---|---|
| LCP | < 2.5s | PageSpeed Insights, CrUX |
| Total image weight | < 2MB/page | Chrome DevTools Network |
| Images above fold | < 500KB | Manual inspection |
| Image requests | < 20 initial | DevTools Network |
| CLS from images | < 0.1 | PageSpeed Insights |
Automated Performance Monitoring
Set up Lighthouse CI to track image metrics over time:
# lighthouserc.js
module.exports = {
ci: {
collect: {
url: [
'https://yoursite.com/',
'https://yoursite.com/category/products',
'https://yoursite.com/product/sample-product'
],
numberOfRuns: 3
},
assert: {
assertions: {
'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'uses-webp-images': 'warn',
'uses-responsive-images': 'warn',
'offscreen-images': 'warn'
}
},
upload: {
target: 'temporary-public-storage'
}
}
};
Before/After Comparison
Document your optimization results:
## Image Optimization Results - [Date]
### Product Listing Page
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Page weight | 8.2MB | 2.1MB | 74% |
| LCP | 4.8s | 1.9s | 60% |
| Image count (initial) | 45 | 12 | 73% |
| Time to Interactive | 6.2s | 3.1s | 50% |
### Product Detail Page
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Page weight | 3.4MB | 1.2MB | 65% |
| LCP | 3.2s | 1.5s | 53% |
| CLS | 0.18 | 0.02 | 89% |
Common Pitfalls and How to Avoid Them
Pitfall 1: Lazy Loading Above-the-Fold Images
Problem: Lazy loading the hero image or first product row increases LCP.
Solution: Audit your templates. Use loading="eager" and fetchpriority="high" for the first 1-3 visible images.
Pitfall 2: Missing Width/Height Attributes
Problem: Layout shifts when images load, hurting CLS score.
Solution: Always include explicit width and height attributes. If dimensions vary, use CSS aspect-ratio:
.product-image {
aspect-ratio: 1 / 1;
width: 100%;
height: auto;
}
Pitfall 3: Serving Desktop Images to Mobile
Problem: Mobile users download 1200px images for 400px containers.
Solution: Implement proper srcset and sizes attributes. Test on real devices, not just browser emulation.
Pitfall 4: Not Compressing Original Uploads
Problem: Optimization efforts wasted on 5MB source images.
Solution: Compress images before upload. Automate with upload validation scripts.
Pitfall 5: Breaking Image SEO
Problem: Aggressive optimization removes alt text or breaks Google Image indexing.
Solution: Keep alt attributes meaningful. Don't block image URLs in robots.txt. Test Google Image search visibility.
FAQ
What's the fastest way to improve LCP on my SuiteCommerce site?
Start with the hero image and first row of product images. Convert to WebP, properly size them, use fetchpriority="high" on the LCP image, and ensure proper cache headers. This alone typically improves LCP by 30-50%.
Should I use a third-party image optimization service?
For sites with large catalogs (10,000+ products), third-party services like Cloudinary, imgix, or Cloudflare Images are worth the cost. They handle format conversion, resizing, and CDN delivery automatically. For smaller catalogs, the techniques in this guide are sufficient.
How do I optimize images in NetSuite's File Cabinet?
NetSuite's File Cabinet doesn't support dynamic image transformations. Either: (1) pre-generate optimized versions and upload separately, (2) use a CDN with image transformation capabilities, or (3) build a SuiteScript service that proxies and transforms images.
Does image optimization affect SEO negatively?
Done correctly, no. WebP and AVIF are fully supported by Google. Lazy loading with proper placeholders doesn't affect indexing. Using descriptive alt text and allowing images to be crawled maintains Google Image visibility.
How much improvement can I realistically expect?
On a typical unoptimized SuiteCommerce site: 40-60% reduction in page weight, 30-50% improvement in LCP, and noticeable improvement in mobile experience. The exact numbers depend on your current state and implementation thoroughness.
Start Optimizing
Image optimization is the highest-ROI performance improvement for most SuiteCommerce sites. The techniques in this guide can be implemented incrementally—start with the quick wins (WebP conversion, lazy loading below-fold images) and progress to the comprehensive extension approach.
If you need help diagnosing image performance issues or implementing these optimizations, our performance team has optimized dozens of SuiteCommerce sites. We're happy to run a free performance audit and identify your specific image bottlenecks.
Every millisecond counts. Your images shouldn't be the reason customers leave.
Need Help with Your NetSuite Project?
Our team of experts is ready to help you achieve your goals.


