Menu
SuiteCommerce Image Optimization: A Developer's Guide
PerformanceImage OptimizationWebPLazy LoadingCDNPerformanceSuiteCommerce

SuiteCommerce Image Optimization: A Developer's Guide

February 3, 202610 min read
Back to Blog

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

  1. Why Images Matter for SuiteCommerce Performance
  2. Image Format Selection: WebP, AVIF, and Fallbacks
  3. Implementing Lazy Loading in SuiteCommerce
  4. Responsive Images for SuiteCommerce Themes
  5. CDN Configuration for Images
  6. Optimizing Product Images at Upload
  7. Building an Image Optimization Extension
  8. Measuring Image Performance Impact
  9. Common Pitfalls and How to Avoid Them
  10. 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

MetricUnoptimizedOptimizedImprovement
Product listing page weight8.2MB2.1MB74% reduction
Time to LCP4.8s1.9s60% faster
Mobile load time (3G)18s+6s66% faster
Image requests4512 (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.

Responsive design across multiple devices

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 TypeMax DimensionsMax File SizeFormat
Product main2000 x 2000px500KBJPEG (q85)
Product alt views2000 x 2000px500KBJPEG (q85)
Lifestyle/hero2400 x 1200px800KBJPEG (q85)
Thumbnails400 x 400px50KBJPEG (q80)
Icons/logos200 x 200px20KBPNG-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.

Fast website loading and image delivery performance

Key Metrics to Track

MetricTargetMeasurement Tool
LCP< 2.5sPageSpeed Insights, CrUX
Total image weight< 2MB/pageChrome DevTools Network
Images above fold< 500KBManual inspection
Image requests< 20 initialDevTools Network
CLS from images< 0.1PageSpeed 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.

Related Articles