SFCC Performance Optimization: A Storefront Audit Checklist
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" threshold | Usual suspects on SFCC |
|---|---|---|
| Largest Contentful Paint (LCP) | 2.5 s or less | Uncached HTML, oversized hero images, render-blocking CSS and JavaScript, web fonts |
| Interaction to Next Paint (INP) | 200 ms or less | Heavy client JavaScript, third-party tags, long hydration tasks on headless storefronts |
| Cumulative Layout Shift (CLS) | 0.1 or less | Images 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
| Question | Tool | Where |
|---|---|---|
| Which controllers are most requested, slowest or least cached? | Technical report | Reports & Dashboards, Technical tab |
| Where inside a request does the time go? | Code Profiler | Administration > Operations > Code Profiler |
| Which integrations are slow or failing? | Service Status | Administration > Operations > Service Status |
| Is custom code hitting platform limits? | Quota Status | Administration > Operations > Quota Status |
| Did errors or warnings spike? | Log Center | Administration > Site Development > Development Setup > Log Center |
| Are jobs competing with the storefront? | Jobs | Administration > Operations > Jobs |
| Is the extra load bots rather than shoppers? | Top Robots | Reports & 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:
| Pipeline | Grid-wide median |
|---|---|
Search-Show | 400 ms |
Product-Show | 300 ms |
Cart-Show | 400 ms |
Home-Show | 90 ms |
Page-Show | 90 ms |
Product-HitTile | 10 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:
| Hotspot | What to look for | Fix |
|---|---|---|
| Per-request hooks | Custom onRequest or onSession logic over 10 ms, Salesforce's stated limit | Move it, cache it or narrow when it runs |
| Post-processed search | Code filtering or sorting search results | Put every criterion into the query |
| Variant loops on listings | Product.getVariants() or Product.getVariationModel() per tile | ProductSearchHit methods (below) |
| Database-heavy APIs on hot pages | Category.getProducts(), Category.getOnlineProducts(), ProductMgr.queryAllSiteProducts(), Product.getPriceModel() | Index-friendly ProductSearchModel APIs |
| Unpaged result sets | Whole result sets processed or rendered | PagingModel with a capped page size; Salesforce's example is 120 products, "View all" included |
| Live calls on browse pages | External calls on home, category, search or product pages | Import or cache the data; if unavoidable, a low timeout (Salesforce's example: 1 second) |
| Long work in a controller | Imports, exports, large loops, transactions near 1,000 modified objects | A job; the web tier closes browser connections after 5 minutes |
| Object bloat | Unneeded session objects and cookies, a wishlist for every anonymous shopper, custom objects never purged | Create 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_promotionvaries 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):
| Quota | Limit (warning) | What it usually means |
|---|---|---|
api.dw.serviceTimeoutNotSet | 0 | An 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 storefront | Storefront 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.collectionSize | 20,000 (12,000) | Large collections built in memory |
api.jsStringLength | 1,000,000 (600,000) | Huge strings, often JSON or HTML assembled in memory |
object.NonStageableCustomObjectPO | 400,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 parameter | Transform object key | Notes |
|---|---|---|
sw, sh | scaleWidth, scaleHeight | 10 to 3,000 pixels |
sm | scaleMode | cut (default) or fit; only with both dimensions |
q | quality | JPG and JP2: 1 to 100, default 80 |
File extension, sfrm | format | Targets include AVIF and WebP; the extension selects the output, sfrm names the source format |
strip | strip | Removes 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=80where 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
asyncordefer. - 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
selectandexpandto return only what you need; categories and variants aren't paginated. - Split short-lived expansions.
availabilitycan 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: truerequest header logs hook timing to Log Center; in PWA Kit,?__server_timing=truereturns 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.
| Finding | Impact | Effort | Order |
|---|---|---|---|
| Hot controller or include not cached | High | Low | 1 |
| Browse-page integration without timeout or circuit breaker | High | Low | 1 |
| Enforced quota violations in production | High | Low to medium | 1 |
| Oversized images on LCP templates | High | Low | 1 |
| Cache clears or replications in peak hours | High | Low (process) | 1 |
| Variant loops or post-processed search | High | Medium | 2 |
Expensive onRequest logic | High (every request) | Medium | 2 |
| Synchronous third-party tags | Medium to high | Low to medium | 2 |
| Client JavaScript and CSS weight | Medium | Medium to high | 3 |
| Re-platforming the storefront | Varies | High | Only with evidence |
Fix in batches and re-measure after each, so every gain maps to a change.
What a good audit report contains
- Scope and baseline: templates, dates, code version, recent replications, traffic.
- Field and lab data per template, side by side: 75th-percentile CrUX values, plus the lab tool and settings.
- Server evidence: Technical report exports, Code Profiler CSVs, Service Status trends, quota log lines.
- One entry per finding: evidence, root cause, fix, owner, impact, effort and the metric that proves the fix.
- An integration inventory: service ID, timeout, circuit breaker, calling pages, fallback.
- A prioritised backlog from the table above.
- 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
URLUtilsorMediaFile, 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
- Troubleshooting Salesforce B2C Commerce Performance: reference medians, the Technical report and a diagnosis sequence.
- Site Performance: Code Profiler modes and the performance and stability coding standards.
- Web Services: service profiles, timeouts, the circuit breaker and Service Status.
- Governance and Quotas: quota actions, Quota Status and quota log files.
- Dynamic Imaging Service: transformation parameters, limits and caching.
- Monitor the Performance of Your Storefront: field vs lab data, CrUX and Core Web Vitals thresholds.