sfcc-performance — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sfcc-performance (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This document outlines key performance optimization strategies for Salesforce B2C Commerce Cloud, focusing on caching and efficient data retrieval.
Ecommerce applications built on Salesforce B2C Commerce can run fast and perform reliably. Use B2C Commerce within its capabilities and ensure that your customizations follow coding best practices. Identify permissible designs that ensure the scalability and robustness of your customizations.
B2C Commerce imposes limits on incoming and outgoing network traffic for B2C Commerce instances and the Content Delivery Network.
These limits are relative to the Gross Merchandise Value (GMV) and are defined in the Main Subscription Agreement (MSA). Data transfers within the limits are at no additional charge (are included in the subscription fee). There is a fee for data transfers exceeding the limits.
When developing your storefront, consider storefront development best practices:
#### Search and Product Processing
OrderableProductsOnly to deal with variation product availabilitydw.catalog.ProductSearchHit.getRepresentedVariationValues() to determine available variation valuesdw.catalog.ProductSearchHit.minPrice or Product.priceMode.minPrice to determine price ranges#### External System Integration
#### Long-Running Operations
#### Concurrency and Data Integrity
EXPORT_STATUS_READY as the last step in order creation. Then order processing jobs can start modifying the order object.#### Transaction Management
#### Critical Page Performance
Replace database intensive or inefficient APIs with appropriate index-friendly APIs. Check code for database intensive APIs in most-visited pages:
#### Avoid These Database-Intensive APIs:
Category.getProducts()Category.getOnlineProducts()Category.getProductAssignments()Category.getOnlineCategoryAssignments()ProductMgr.queryAllSiteProducts()Product.getPriceModel()Product.getVariants()Product.getVariationModel()#### Use These Index-Friendly APIs Instead:
ProductSearchModel.search()ProductSearchModel.orderableProductsOnly(true)ProductSearchModel.getRefinements()ProductSearchRefinements.getNextLevelRefinementValues()ProductSearchModel.getProductSearchHits()ProductSearchHit.getMinPrice()ProductSearchHit.getMaxPrice()ProductSearchHit.getRepresentedProductIDs()ProductSearchHit.getRepresentedVariationValues(attribute)#### OCAPI Specific Requirements:
ProductMgr.getProduct() or product.getVariations()#### SFRA Specific Requirements:
To optimize job performance, follow the job development standards:
#### Import and Data Processing
#### Data Quality and Validation
#### Memory Management
When processing large data sets, pay attention to the memory footprint:
#### Concurrency and Resource Management
#### Recovery and Reliability
The web-server page cache is the most critical performance feature for server-rendered storefronts. The goal is to serve fully rendered HTML from this cache to avoid hitting the application server.
Control caching within your controller using the response object. This is superior to the legacy <iscache> tag.
response.setExpires(milliseconds): Sets a cache duration for the entire page response.
Example:
// cartridge/controllers/Product.js
var server = require('server');
server.get('Show', function (req, res, next) {
// Cache for 24 hours
var oneDay = 24 * 60 * 60 * 1000;
response.setExpires(Date.now() + oneDay);
res.render('product/productDetails');
next();
});Use remote includes (<isinclude url="..." />) to assemble pages from components with different cache policies. A long-cached main page can include a dynamic, non-cached header with user-specific info. [1, 2]
Anti-Pattern: Avoid creating remote includes with unique URL parameters for each item in a list (e.g., &position=1, &position=2). This creates an N+1 request problem at the HTTP level and defeats the cache. [1, 3]
The cache key is the full URL. To maximize the cache hit ratio:
Administration > Sites > Feature Switches) to ignore marketing parameters (e.g., utm_source, utm_campaign) when generating the cache key. [4, 5]response.setVaryBy('price_promotion') to create separate cache entries for users with different prices or promotions. Use this carefully, as it can fragment the cache. [4, 5]CacheMgr)Use custom caches for application-level data caching within dynamic requests (e.g., cart, checkout) where page caching isn't possible.
Use Cases:
{
"caches": [
{
"id": "ExternalAPICache",
"expireAfterSeconds": 300
}
]
} {
"caches": "./caches.json"
}The package.json must be in the cartridge root (cartridges/{{mycartridge}}/package.json), and the caches.json path is relative to that.
null, not undefined).get(key, loader)Prefer the loader form so the expensive work only runs on cache miss:
var CacheMgr = require('dw/system/CacheMgr');
var Site = require('dw/system/Site');
var cache = CacheMgr.getCache('ExternalAPICache');
function getSiteScopedValue(keySuffix, loader) {
var key = Site.current.ID + '_' + keySuffix;
return cache.get(key, loader);
}Custom caches can be cleared by operational events (code changes/activation, replications). Design code so cache eviction is safe and does not break core flows.
Always use the atomic cache.get(key, loader) method to prevent a "thundering herd" problem on cache misses. [6, 9]
Example:
var CacheMgr = require('dw/system/CacheMgr');
var MyHTTPService = require('~/cartridge/scripts/services/myHTTPService');
function getExternalData() {
var apiCache = CacheMgr.getCache('ExternalAPICache');
var cacheKey = 'myExternalData';
// get() executes the loader function ONLY on a cache miss.
var data = apiCache.get(cacheKey, function () {
// This expensive call only runs if data is not in cache.
var result = MyHTTPService.getService().call();
return result.ok ? JSON.parse(result.object.text) : null;
});
return data;
}Key Limitation: Custom caches are local to each application server pod and are not a distributed, instance-wide cache. Data stored on one pod is not visible to others.
This is a critical performance distinction.
NEVER use ProductMgr.getProduct() inside a loop over ProductSearchModel results. This causes one fast index query followed by N slow database queries, which will crash a site under load.
Incorrect (Anti-Pattern):
// In a PLP template, looping over search results
var psm = new ProductSearchModel();
//... configure psm...
psm.search();
var hits = psm.getProductSearchHits();
while (hits.hasNext()) {
var hit = hits.next();
// ANTI-PATTERN: Calling ProductMgr in a loop!
var product = ProductMgr.getProduct(hit.getProductID());
//... do something with the full product object...
}Correct:
Use the ProductSearchHit object directly. It contains all necessary data from the index for display on a listing page. If data is missing, add it to the search index configuration.
// In a PLP template, looping over search results
var psm = new ProductSearchModel();
//... configure psm...
psm.search();
var hits = psm.getProductSearchHits();
while (hits.hasNext()) {
var hit = hits.next();
// CORRECT: Use the hit object directly for name, price, etc.
var minPrice = hit.getMinPrice();
var variationValues = hit.getRepresentedVariationValues('color');
//... render tile using data from 'hit'...
}The caching models for OCAPI and SCAPI are fundamentally different.
One of the Commerce Cloud application layer components performs web-tier caching for SCAPI GET requests across multiple API families. This cache lives on the server side and is applied only after a request reaches the platform. Any additional caching layers you add (CDN, browser, SPA state) operate independently—you could have a cache miss on the web tier but a hit in your edge cache, and vice versa. Plan your caching strategy with this multi-layer reality in mind.
When personalization is enabled for a SCAPI resource, the cache key includes the following in addition to the URL string:
The platform keeps separate cache entries for each combination. For example, if shopper A qualifies for promotion X and shopper B qualifies for promotion Y, the same product URL produces two cache entries. This segmentation can be powerful but also multiplies cache storage. Use personalization only when you have well-sized groups and a clear business reason.
By default, product requests that expand prices or promotions—and product search requests with the prices expand—are already personalized. Calling response.setVaryBy('price_promotion') in a script reinforces that behavior. Note that price_promotion is the only supported value; other strings have no effect.
Use the Script API to adjust cache policies dynamically:
dw.system.Response#setExpires(milliseconds): Sets an explicit expiration timestamp. The value must be at least 1,000 ms in the future and no more than 86,400,000 ms (24 hours).dw.system.Response#setVaryBy('price_promotion'): Opts into personalized caching for price- or promotion-sensitive responses.exports.modifyGETResponse = function (scriptCategory, categoryWO) {
// Cache for one hour instead of the default TTL
response.setExpires(Date.now() + 3_600_000);
// Optional: personalize by price & promotion eligibility
response.setVaryBy('price_promotion');
return new Status(Status.OK);
};ProductMgr.getProduct().Example (SCAPI):
// Client makes a call with a custom parameter
// GET /shopper-products/v1/.../products/my-prod?c_view=light
// SCAPI Hook Script (product.js)
exports.modifyResponse = function (product, productResponse) {
// Logic is conditional on the custom parameter
if (request.httpParameters.c_view === 'light') {
delete productResponse.long_description;
}
};This creates a separate, cacheable version of the response for the c_view=light URL.
SCAPI expand Parameter: The cache TTL for a SCAPI response is determined by the lowest TTL of all requested expand parameters. Avoid requesting volatile data (like availability, 60s TTL) alongside stable data (like images, 24hr TTL).
You can build your own REST endpoints that integrate with SCAPI's caching, security, and other framework features.
Custom endpoints can leverage the same powerful web-tier page cache. Enable it by calling response.setExpires() in your implementation script.
Example:
// cartridge/rest-apis/my-api/v1/script.js
var RESTResponseMgr = require('dw/system/RESTResponseMgr');
exports.getLoyaltyInfo = function (params) {
var loyaltyData = { id: params.c_customer_id, points: 1234 };
// Cache this custom API response for 5 minutes
response.setExpires(Date.now() + (5 * 60 * 1000));
RESTResponseMgr.createSuccess(loyaltyData).render();
};
exports.getLoyaltyInfo.public = true;For maximum resilience and performance when calling external systems, combine both cache layers:
response.setExpires().This pattern ensures that most requests are served instantly from the web-tier, and even on a miss, the data is likely served from the fast application-tier cache, minimizing slow calls to the external dependency.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.