General Guide: See Global CLS Guide for universal concepts and fixes.
SAP Commerce-Specific CLS Causes
1. Spartacus Component Loading
Angular components rendering asynchronously:
// Problem: Async component causes shift
<ng-container *ngIf="product$ | async as product">
<cx-product-summary [product]="product"></cx-product-summary>
</ng-container>
2. OCC API Data Loading
Late-loading product data causing layout shifts:
Initial render → API response → Content injection → Shift
3. Dynamic SmartEdit Content
CMS content slots loading after page render.
4. Price and Stock Updates
Asynchronous pricing and availability updates.
SAP Commerce-Specific Fixes
Fix 1: Reserve Space for Components
Use skeleton screens in Spartacus:
// product-summary.component.html
<div class="product-summary" [class.loading]="isLoading">
<ng-container *ngIf="product; else skeleton">
<h1>{{ product.name }}</h1>
<cx-product-price [product]="product"></cx-product-price>
</ng-container>
<ng-template #skeleton>
<div class="skeleton-title"></div>
<div class="skeleton-price"></div>
</ng-template>
</div>
// product-summary.component.scss
.product-summary {
min-height: 200px; // Reserve space
.skeleton-title {
height: 32px;
width: 60%;
background: #f0f0f0;
margin-bottom: 16px;
animation: pulse 1.5s infinite;
}
.skeleton-price {
height: 24px;
width: 30%;
background: #f0f0f0;
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
Fix 2: Optimize Async Pipe Usage
Pre-fetch data before route activation:
// product.resolver.ts
@Injectable({ providedIn: 'root' })
export class ProductResolver implements Resolve<Product> {
constructor(private productService: ProductService) {}
resolve(route: ActivatedRouteSnapshot): Observable<Product> {
const code = route.params['productCode'];
return this.productService.get(code).pipe(
take(1),
tap(product => {
// Pre-populate component state
this.productService.setCurrentProduct(product);
})
);
}
}
Fix 3: Stabilize CMS Slots
Reserve space for SmartEdit content:
// Layout styles
.content-slot {
&[data-slot="Section1"] {
min-height: 400px; // Hero banner area
}
&[data-slot="Section2A"] {
min-height: 300px; // Product carousel
}
&[data-slot="BottomContent"] {
min-height: 200px; // Footer content
}
}
Fix 4: Handle Price Updates Gracefully
Prevent layout shift during price updates:
// price.component.ts
@Component({
selector: 'cx-product-price',
template: `
<div class="price-container" [style.min-width.px]="minWidth">
<span *ngIf="price$ | async as price; else loading">
{{ price.formattedValue }}
</span>
<ng-template #loading>
<span class="price-loading">---</span>
</ng-template>
</div>
`,
styles: [`
.price-container {
display: inline-block;
min-width: 80px;
text-align: right;
}
.price-loading {
color: transparent;
background: #f0f0f0;
}
`]
})
export class PriceComponent {
@Input() product: Product;
minWidth = 80; // Reserve consistent width
}
Fix 5: Stabilize Product Grid
Set fixed dimensions for product tiles:
// product-list.component.scss
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 24px;
}
.product-tile {
aspect-ratio: 3/4;
display: flex;
flex-direction: column;
.product-image {
flex: 1;
aspect-ratio: 1/1;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.product-info {
padding: 16px;
min-height: 120px;
}
}
Fix 6: Handle Font Loading
Prevent FOUT (Flash of Unstyled Text):
// styles.scss
@font-face {
font-family: 'SAP-Icons';
src: url('/assets/fonts/SAP-Icons.woff2') format('woff2');
font-display: block; // Or 'optional' for less critical fonts
}
// Fallback with similar metrics
body {
font-family: 'SAP 72', -apple-system, BlinkMacSystemFont, sans-serif;
}
Monitoring CLS
// Track CLS in Spartacus
import { onCLS } from 'web-vitals';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class WebVitalsService {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
this.trackCLS();
}
}
private trackCLS(): void {
onCLS(metric => {
window.dataLayer?.push({
event: 'web_vitals',
vital_name: 'CLS',
vital_value: metric.value,
vital_rating: metric.rating
});
});
}
}
CLS Targets
| Rating | CLS Value |
|---|---|
| Good | ≤ 0.1 |
| Needs Improvement | 0.1 - 0.25 |
| Poor | > 0.25 |