Skip to main content
OZAN ALTINBAS
Performance & SEO

SFCC Performance Optimization: A Storefront Audit Checklist

By 13 min read

Salesforce describes B2C Commerce performance as a shared responsibility: Salesforce owns the platform, and you own the performance of your customisations (Monitor Your Site). In practice, a slow storefront usually comes down to a few specific problems: a listing page that loads every variant, an integration with no sensible timeout, a controller that was never cached, a hero image served at its upload size.

This checklist is the order to audit an SFRA or headless storefront in: measure, profile the server, check integrations, caching and quotas, then the front end, and finally rank the findings. It links to deeper guides instead of repeating them. For the platform overview, start with the SFCC guide.


Step 0: Scope the audit

  • Pick the templates that carry revenue. Salesforce's coding standards name the most visited pages, which must perform well and be cached where possible: search and category (Search-Show), product detail (Product-Show), home (Home-Show), cart (Cart-Show) and checkout.
  • Find the real first pages. The Site Performance guide notes that the top entry page can be an email or partner landing page rather than the home page.
  • Freeze a baseline. Record the code version, recent replications, promotions and traffic, and agree when you will re-measure.

Step 1: Measure before you change anything

Field data decides, lab data explains

Salesforce's storefront monitoring guide distinguishes lab data, collected in controlled tests during development, from field data, which captures real shoppers after release. Google uses field data from the Chrome UX Report (CrUX) for page experience ranking, not Lighthouse scores. Per the same page, CrUX is a 28-day rolling average assessed at the 75th percentile, focuses on hard navigations (so route changes inside a single-page app are under-represented) and doesn't include iOS devices.

Core Web Vital"Good" thresholdUsual suspects on SFCC
Largest Contentful Paint (LCP)2.5 s or lessUncached HTML, oversized hero images, render-blocking CSS and JavaScript, web fonts
Interaction to Next Paint (INP)200 ms or lessHeavy client JavaScript, third-party tags, long hydration tasks on headless storefronts
Cumulative Layout Shift (CLS)0.1 or lessImages without dimensions, late slots and banners, font swaps, consent bars

For lab data, Salesforce lists Lighthouse, PageSpeed Insights, WebPageTest and the Chrome DevTools panels. Test from outside the office network: internal LAN tests don't reflect shoppers in other regions or eCDN edge caching.

Server data: the Business Manager toolset

QuestionToolWhere
Which controllers are most requested, slowest or least cached?Technical reportReports & Dashboards, Technical tab
Where inside a request does the time go?Code ProfilerAdministration > Operations > Code Profiler
Which integrations are slow or failing?Service StatusAdministration > Operations > Service Status
Is custom code hitting platform limits?Quota StatusAdministration > Operations > Quota Status
Did errors or warnings spike?Log CenterAdministration > Site Development > Development Setup > Log Center
Are jobs competing with the storefront?JobsAdministration > Operations > Jobs
Is the extra load bots rather than shoppers?Top RobotsReports & Dashboards, Traffic tab

Reports & Dashboards data lags events by 24 hours: use it for trends, not live incidents.

Salesforce's reference medians

The B2C Commerce performance troubleshooting guide publishes grid-wide median response times for the most critical pipelines. If your critical controllers sit around these values, Salesforce considers the site scalable:

PipelineGrid-wide median
Search-Show400 ms
Product-Show300 ms
Cart-Show400 ms
Home-Show90 ms
Page-Show90 ms
Product-HitTile10 ms
OCAPI-Request (all OCAPI requests)250 ms

These are server times; end-to-end times from external monitoring are expected to be much higher. Product-HitTile is the SiteGenesis product tile; on SFRA, compare Tile-Show.


Step 2: Profile the server side

Read the Technical report first

In the Technical tab, sort the main controller requests by request count, descending. For each busy controller, check response time and the cache columns, then open its remote includes, often where the time goes.

Run the Code Profiler without hurting production

The Site Performance guide documents three modes:

  • Production Mode samples a subset of requests: the default on non-sandbox instances, always on, minimal impact.
  • Development Mode measures every request: the default on sandboxes, with some overhead.
  • Extended Script Development Mode adds functions and single lines; Salesforce warns of severe performance implications in production.

The screen shows only the application server serving your Business Manager session, and an hourly job exports the data to CSV, then resets it. Read Elapsed Time with Wait Time (database or service I/O, or locking): high elapsed time with low wait time points to script work, high wait time to queries or integrations. Filter by result type: Controller (SCRIPT_CONTROLLER), Script details (SCRIPT_API, SCRIPT_HOOK) and View (ISML).

Server-side hotspots

From the Site Performance guide's coding standards:

HotspotWhat to look forFix
Per-request hooksCustom onRequest or onSession logic over 10 ms, Salesforce's stated limitMove it, cache it or narrow when it runs
Post-processed searchCode filtering or sorting search resultsPut every criterion into the query
Variant loops on listingsProduct.getVariants() or Product.getVariationModel() per tileProductSearchHit methods (below)
Database-heavy APIs on hot pagesCategory.getProducts(), Category.getOnlineProducts(), ProductMgr.queryAllSiteProducts(), Product.getPriceModel()Index-friendly ProductSearchModel APIs
Unpaged result setsWhole result sets processed or renderedPagingModel with a capped page size; Salesforce's example is 120 products, "View all" included
Live calls on browse pagesExternal calls on home, category, search or product pagesImport or cache the data; if unavoidable, a low timeout (Salesforce's example: 1 second)
Long work in a controllerImports, exports, large loops, transactions near 1,000 modified objectsA job; the web tier closes browser connections after 5 minutes
Object bloatUnneeded session objects and cookies, a wishlist for every anonymous shopper, custom objects never purgedCreate only when needed; purge jobs

Live calls matter most: per Salesforce, a thread waiting for an external system can't serve other requests, and many waiting threads can make the whole cluster unresponsive.

Replace variant loops with search hits

ProductSearchHit answers price-range and variation-value questions from the search index. The ProductSearchModel reference deprecates getProducts() because loading the products for every result is very expensive.

'use strict';

var ProductSearchModel = require('dw/catalog/ProductSearchModel');
var PagingModel = require('dw/web/PagingModel');

// Anti-pattern on a listing page: touches every variant of every product
// master.getVariants().toArray().forEach(function (variant) { ... });

function getTiles(categoryId, start, pageSize) {
    var psm = new ProductSearchModel();
    psm.setCategoryID(categoryId);
    psm.setOrderableProductsOnly(true); // availability filtering in the query, not in code
    psm.search();

    var paging = new PagingModel(psm.getProductSearchHits(), psm.getCount());
    paging.setStart(start);
    paging.setPageSize(Math.min(pageSize, 120)); // cap the page size, "View all" included

    var tiles = [];
    var hits = paging.getPageElements(); // call once for an iterator source
    while (hits.hasNext()) {
        var hit = hits.next();
        tiles.push({
            id: hit.getProductID(),
            minPrice: hit.getMinPrice(), // index prices: no price model per variant
            maxPrice: hit.getMaxPrice(),
            colors: hit.getRepresentedVariationValues('color') // only the variants that matched
        });
    }
    return tiles;
}

module.exports = { getTiles: getTiles };

Two caveats from the reference: index prices can differ from ProductPriceModel prices, and hits can include products taken offline since the last index update.


Step 3: Integrations and the service framework

Salesforce's standard is that every direct third-party HTTP call moves to the service framework (Web Services), which owns timeouts, circuit breakers and rate limits.

Timeouts

The timeout is set in milliseconds on the service profile (Administration > Operations > Services, Profiles tab). It applies to the connection and to each read, not to the whole call: in Salesforce's example, with a 5,000 ms timeout, a 4-second connection followed by two 4-second reads takes about 13 seconds without timing out.

The quota api.dw.serviceTimeoutNotSet flags FTP, SFTP, HTTP and web service calls with no timeout or a timeout of 30 seconds or more (Salesforce Help). Treat every hit as a finding.

Circuit breaker and rate limiter

  • Enable Circuit Breaker is checked by default. Salesforce recommends keeping it on with Max Circuit Breaker Calls blank or zero, which enables the system-level circuit breaker. If you set the calls, also set Circuit Breaker Interval (ms).
  • The breaker counts connection failures, protocol errors such as HTTP 500 and 503, and exceptions thrown during the call. HTTP 4xx responses don't count.
  • Enable Rate Limit caps outgoing calls per interval.

When either one trips, no call is made. In that case, and when a call times out, the dw.svc.Result has the status SERVICE_UNAVAILABLE and an unavailableReason of CIRCUIT_BROKEN, RATE_LIMITED or TIMEOUT. Design the fallback before you need it:

'use strict';

var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');
var Logger = require('dw/system/Logger');

// Timeout, circuit breaker and rate limit live on the service profile
// in Business Manager, not in code.
function getStockService() {
    return LocalServiceRegistry.createService('example.stock.http', {
        createRequest: function (svc, params) {
            svc.setRequestMethod('GET');
            svc.addParam('sku', params.sku);
        },
        parseResponse: function (svc, client) {
            return JSON.parse(client.text);
        }
    });
}

function getStock(sku) {
    var result = getStockService().call({ sku: sku });
    if (result.ok) {
        return result.object;
    }
    Logger.warn('Stock lookup failed: {0} {1}', result.status, result.unavailableReason || result.errorMessage);
    return null; // degrade: render the page without live stock
}

module.exports = { getStock: getStock };

The sample doesn't cache stock, which is live state. For responses that depend on the URL alone, HTTPService.setCachingTTL() is easy to miss. Per the HTTPService reference, it caches 2xx GET responses with a content length under 50 KB, keyed by URL and user name, ignoring remote cache headers.

Service Status (Administration > Operations > Service Status) shows each service's performance in near real time and keeps the data for up to 10 days.


Step 4: Check caching coverage

The SFCC caching guide covers the mechanics. In an audit, check that:

  • Every hot controller and remote include has a cache directive. One that is never served from cache in the Technical report isn't cached.
  • Nothing personalised sits in a cached response beyond what price_promotion varies by.
  • URLs don't carry unique parameters. The troubleshooting guide warns about email campaigns with unique URL parameters.
  • Cache clears are timed well. Salesforce's common mistakes include invalidating just before a sales peak, several times a day for a few changes, or to publish static content. An <iscache type="daily"> tag also expires every page of that type at once.
  • Replications avoid peak hours. Code replication, code activation and data replication clear the cache unless the job says otherwise.
  • Nobody warms the cache with a spider, which Salesforce says costs significant resources for little benefit.
  • Reads use GET. Only GET requests can be page cached.

Step 5: Treat quota warnings as performance signals

Governance and Quotas treats a violation as a sign that custom code is using the platform inefficiently. Each quota has one of three actions:

  • log_only: under evaluation by Salesforce; ignore it for now.
  • warn: act at the warning threshold (60% of the limit). Usage beyond the limit isn't blocked, but instability can follow.
  • error: enforced. The operation is blocked, and a storefront request shows the general error page.

Don't catch quota exceptions; avoid them by design. Quota Status shows the last 24 hours (or since the last restart) in green, orange and red, and can email a daily summary of violations. The quota log files in the WebDAV log folder name the site, the top pipeline and often the template or script. Salesforce's standard: no enforced violations on staging or production, and quota alerts subscribed by all site admins.

Quotas that point straight at performance problems (API Quotas, Object Quotas):

QuotaLimit (warning)What it usually means
api.dw.serviceTimeoutNotSet0An outbound call without a sensible timeout
api.dw.net.HTTPClient.send()16 per storefront page (10)Too many remote calls in one request
api.dw.catalog.ProductMgr.queryAllProducts()0 in the storefrontStorefront code querying the whole catalog
api.dw.object.CustomObjectMgr.create()10 per storefront page (6)Custom objects used as a write-heavy store
api.dw.util.collectionSize20,000 (12,000)Large collections built in memory
api.jsStringLength1,000,000 (600,000)Huge strings, often JSON or HTML assembled in memory
object.NonStageableCustomObjectPO400,000 per instance (240,000)Custom object volume without purge jobs

Step 6: Front end: images, JavaScript and third parties

Images through the Dynamic Imaging Service

Generate image URLs with URLUtils or MediaFile: per the Dynamic Imaging Service documentation, the API validates the parameters and protects you from changes to the URL pattern. Never paste /dw/image/v2/ or a hostname into a template.

URL parameterTransform object keyNotes
sw, shscaleWidth, scaleHeight10 to 3,000 pixels
smscaleModecut (default) or fit; only with both dimensions
qqualityJPG and JP2: 1 to 100, default 80
File extension, sfrmformatTargets include AVIF and WebP; the extension selects the output, sfrm names the source format
stripstripRemoves EXIF data and colour profiles; default true

Images over 10 MB aren't transformed, the maximum is 3,000 × 3,000 pixels, and transformations over 29 seconds return HTTP 408.

// In a model or controller, never hard-coded in ISML
var image = product.getImage('large', 0); // first image of the view type
var tileUrl = image ? image.getAbsImageURL({ scaleWidth: 400, format: 'jpg' }).toString() : null;
  • Size every slot. Request the width each slot renders at, with a few fixed widths in srcset.
  • Offer modern formats. Salesforce's AVIF and WebP guide recommends a <picture> element with AVIF and WebP sources and a JPEG fallback, each URL generated in script.
  • Keep parameters stable. Identical parameters map to one CDN cache key; omit q=80 where 80 is already the default.
  • Don't lazy-load the LCP image. Lazy-load what sits below the fold.

JavaScript, CSS and fonts

SFRA compiles client JavaScript and SCSS with sgmf-scripts (npm run compile:js, npm run compile:scss). Audit what that build ships with the DevTools Coverage panel, which Salesforce recommends for finding unused code. Salesforce's PWA Kit performance guide recommends self-hosted, subsetted WOFF2 fonts and a deliberate font-display value, which holds for SFRA too. Remove avoidable redirects, and keep cookies small because they travel with every request.

Third-party tags

The same guide's rules for third-party scripts apply to any storefront:

  • Audit every external script and remove those that don't justify their cost.
  • Never load them synchronously; use async or defer.
  • Lazy-load chat and social widgets on scroll or click.
  • Gate marketing tags behind a consent management platform.

Step 7: Headless specifics

On PWA Kit or any SCAPI front end, the server rules above apply to hooks and Custom APIs. Salesforce's Shopper API performance guide adds:

  • Know who owns what. Salesforce tests the out-of-the-box Shopper APIs; with hooks you own your code's performance, with Custom APIs all of it.
  • Trim responses. Use select and expand to return only what you need; categories and variants aren't paginated.
  • Split short-lived expansions. availability can only be cached for 60 seconds; fetch it separately so the rest caches longer.
  • Respect the 10-second limit. Slower Shopper API responses return HTTP 504.
  • Time your hooks. The sfdc_verbose: true request header logs hook timing to Log Center; in PWA Kit, ?__server_timing=true returns a Server-Timing header for the rendering phases.
  • Watch the CDN. The Metrics Dashboard in Log Center shows Managed Runtime request time and cache hit ratio.

For the architecture trade-offs, see SFRA vs PWA Kit; for rate limits and load shedding, SCAPI vs OCAPI; for hook design, the SFCC hooks guide.


Step 8: Jobs and operations

Background work competes with shoppers for the same instance. Salesforce's rules:

  • Job load factor: total job seconds on an instance per day divided by 86,400; Salesforce says to keep it below 0.20.
  • Long jobs: analyse any job running over an hour, and stagger start times.
  • Search index and backups: a full index rebuild and a Site Backup on staging at most once a day each.
  • Peak hours: no page cache invalidation, code replication or data replication.
  • Load tests: notify Support through a case, ideally a week ahead, with the test window, expected traffic and load generator IPs.

Prioritise: impact × effort

These ratings are a starting point; adjust them with your evidence.

FindingImpactEffortOrder
Hot controller or include not cachedHighLow1
Browse-page integration without timeout or circuit breakerHighLow1
Enforced quota violations in productionHighLow to medium1
Oversized images on LCP templatesHighLow1
Cache clears or replications in peak hoursHighLow (process)1
Variant loops or post-processed searchHighMedium2
Expensive onRequest logicHigh (every request)Medium2
Synchronous third-party tagsMedium to highLow to medium2
Client JavaScript and CSS weightMediumMedium to high3
Re-platforming the storefrontVariesHighOnly with evidence

Fix in batches and re-measure after each, so every gain maps to a change.


What a good audit report contains

  1. Scope and baseline: templates, dates, code version, recent replications, traffic.
  2. Field and lab data per template, side by side: 75th-percentile CrUX values, plus the lab tool and settings.
  3. Server evidence: Technical report exports, Code Profiler CSVs, Service Status trends, quota log lines.
  4. One entry per finding: evidence, root cause, fix, owner, impact, effort and the metric that proves the fix.
  5. An integration inventory: service ID, timeout, circuit breaker, calling pages, fallback.
  6. A prioritised backlog from the table above.
  7. A re-measurement plan allowing for the 28-day CrUX window and the 24-hour Reports & Dashboards delay.

Key takeaways

  • Field data (CrUX at the 75th percentile) shows outcomes; lab data and the Code Profiler show causes.
  • Compare critical controllers with Salesforce's published medians first.
  • Route every integration through the service framework, with a deliberate timeout and the circuit breaker on.
  • Quota warnings are early alarms; enforced violations are errors shoppers see.
  • Build image URLs with URLUtils or MediaFile, sized to the slot, in modern formats.
  • Fix high-impact, low-effort findings first, then re-measure.

If you want an independent performance audit of your storefront, see how I work on the service page or get in touch.

Further reading