Cumulative Layout Shift (CLS) measures visual stability by tracking unexpected layout shifts during page load. High CLS frustrates users and negatively impacts SEO.
CLS Performance Targets
| Rating | CLS Score | User Impact |
|---|---|---|
| Good | 0-0.1 | Stable, no shifts |
| Needs Improvement | 0.1-0.25 | Noticeable shifts |
| Poor | >0.25 | Jarring, poor UX |
Goal: Keep CLS under 0.1 for excellent user experience.
Understanding CLS in Ecwid
Common Causes of Layout Shift
Ecwid-Specific Issues:
- Widget initialization: Ecwid content loads after page, causing shift
- Dynamic product images: Images load without reserved space
- Cart updates: Cart count/total updates shift layout
- Third-party scripts: Ads, analytics inject content
- Font loading: Web fonts cause text reflow
How Layout Shifts Happen
1. Page loads → Host page renders
2. Ecwid script loads → Widget initializes
3. Content appears → Layout shifts down/around
4. Images load → More shifting
5. Fonts swap → Text reflows
Each shift contributes to CLS score.
Measuring CLS
Use PageSpeed Insights
- Go to PageSpeed Insights
- Enter your store URL
- View CLS score and shifting elements
- Check "View Treemap" to see what shifted
Use Chrome DevTools
Enable Layout Shift Regions:
- Open DevTools (F12)
- Press Cmd/Ctrl + Shift + P
- Type "Rendering"
- Select "Show Rendering"
- Check "Layout Shift Regions"
- Reload page - see blue flashes where shifts occur
Use Lighthouse
lighthouse https://your-ecwid-store.com --view
Look for CLS score in Performance section.
Use Web Vitals Library
<script type="module">
import {getCLS} from 'https://unpkg.com/web-vitals@3/dist/web-vitals.js?module';
getCLS(function(metric) {
console.log('CLS:', metric.value);
// Send to GA4
gtag('event', 'web_vitals', {
event_category: 'Web Vitals',
event_label: 'CLS',
value: Math.round(metric.value * 1000),
non_interaction: true
});
});
</script>
Optimization Strategies
1. Reserve Space for Ecwid Widget
Pre-allocate space where Ecwid will render to prevent shift.
Set Minimum Height
<div id="my-store-123456" style="min-height: 800px;">
<!-- Ecwid widget loads here -->
</div>
Or with CSS:
#my-store-123456 {
min-height: 800px; /* Adjust based on typical content */
}
/* Mobile */
@media (max-width: 768px) {
#my-store-123456 {
min-height: 600px;
}
}
Use Skeleton Loader
Display placeholder content while Ecwid loads:
<div id="my-store-123456">
<div class="ecwid-skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-grid">
<div class="skeleton-product"></div>
<div class="skeleton-product"></div>
<div class="skeleton-product"></div>
</div>
</div>
</div>
<style>
.ecwid-skeleton {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.skeleton-header {
height: 60px;
background: #f0f0f0;
margin-bottom: 20px;
animation: pulse 1.5s infinite;
}
.skeleton-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.skeleton-product {
height: 400px;
background: #f0f0f0;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Hide skeleton when Ecwid loads */
.ecwid .ecwid-skeleton {
display: none;
}
</style>
<script>
Ecwid.OnPageLoad.add(function() {
// Remove skeleton when Ecwid loads
document.querySelector('.ecwid-skeleton')?.remove();
});
</script>
Add script to Custom JavaScript Code section.
2. Optimize Product Images
Images loading without dimensions cause major shifts.
Specify Image Dimensions
Always set width and height attributes:
<!-- Bad: no dimensions -->
<img src="product.jpg" alt="Product">
<!-- Good: dimensions specified -->
<img src="product.jpg" alt="Product" width="500" height="500">
Use Aspect Ratio Containers
For responsive images:
.product-image-container {
position: relative;
width: 100%;
padding-bottom: 100%; /* 1:1 aspect ratio (square) */
overflow: hidden;
}
.product-image-container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
<div class="product-image-container">
<img src="product.jpg" alt="Product" loading="lazy">
</div>
Ecwid Product Image Settings
Settings → General → Instant Site
Image sizes - Set consistent dimensions:
- Product gallery: 1000x1000px
- Thumbnails: 400x400px
Upload square images to avoid unexpected crops/scaling
Preload Critical Images
For hero images or featured products:
<head>
<link rel="preload" as="image" href="https://cdn-ecwid.com/hero-image.webp">
</head>
3. Optimize Font Loading
Font swaps cause text to reflow, shifting layout.
Use font-display: optional
Prevents reflow by showing fallback if font isn't ready:
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: optional; /* Prevents font swap shift */
}
Trade-off: Font may not load on slow connections. Alternative:
font-display: swap; /* Allows swap but may cause shift */
Choose based on priorities:
- optional: Zero CLS from fonts, may show fallback
- swap: Always shows custom font, may cause CLS
Preload Fonts
Load critical fonts early:
<head>
<link rel="preload" href="/fonts/main-font.woff2" as="font" type="font/woff2" crossorigin>
</head>
Match Fallback Font Metrics
Minimize shift by matching fallback font size to custom font:
body {
font-family: 'CustomFont', Arial, sans-serif;
/* Adjust fallback to match custom font metrics */
font-size-adjust: 0.5; /* Adjust based on your fonts */
}
Use System Fonts
Zero shift option - use system fonts:
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
4. Handle Dynamic Content Carefully
Cart Counter/Total
Cart updates can shift layout if not handled properly.
Bad (causes shift):
<div class="cart-total">
<!-- Content injected here, pushing down page -->
</div>
Good (reserved space):
<div class="cart-total" style="min-height: 40px; display: flex; align-items: center;">
<!-- Content injected here, no shift -->
</div>
Banner/Alert Messages
Reserve space for banners:
.promo-banner {
height: 50px; /* Fixed height */
overflow: hidden;
transition: opacity 0.3s;
}
.promo-banner:empty {
opacity: 0; /* Hide when empty but maintain space */
}
5. Optimize Third-Party Embeds
Ads, social media widgets, etc. often cause shifts.
Reserve Space for Ads
<div class="ad-container" style="width: 300px; height: 250px;">
<!-- Ad loads here -->
</div>
Use Facades for Embeds
Instead of loading full YouTube/social embeds immediately:
<div class="youtube-facade" style="width: 560px; height: 315px; position: relative;">
<img src="youtube-thumbnail.jpg" style="width: 100%; height: 100%;">
<button Play</button>
</div>
<script>
function loadVideo(btn) {
const container = btn.parentElement;
const iframe = document.createElement('iframe');
iframe.src = 'https://www.youtube.com/embed/VIDEO_ID';
iframe.style.width = '100%';
iframe.style.height = '100%';
container.innerHTML = '';
container.appendChild(iframe);
}
</script>
6. Prevent Script-Injected Content Shifts
Load Scripts Asynchronously
<!-- Bad: blocks rendering, then shifts content -->
<script src="widget.js"></script>
<!-- Good: loads without blocking -->
<script src="widget.js" async></script>
Cookie Banners
Cookie consent banners often cause shifts.
Solution: Position fixed or absolute:
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
/* Overlays content instead of pushing it */
}
Live Chat Widgets
Chat widgets that push content up cause CLS.
Solution:
.chat-widget {
position: fixed !important;
bottom: 20px !important;
right: 20px !important;
/* Doesn't affect page layout */
}
Add to your CSS, or wrap chat embed code:
<div style="position: fixed; bottom: 20px; right: 20px; z-index: 9999;">
<!-- Chat widget code here -->
</div>
7. Optimize Ecwid Settings
Disable Unnecessary Features
Features that inject content can cause shifts:
- Settings → General → Instant Site
- Disable:
- Unnecessary widgets
- Extra navigation elements
- Auto-appearing popups
Configure Product Grid
Set fixed grid dimensions:
.ecwid .ec-grid__cell {
min-height: 400px; /* Prevents height changes during load */
}
Add to your custom CSS.
8. Test on Real Devices
CLS often varies by device and connection speed.
Test on:
- Desktop: Chrome DevTools device emulation
- Mobile: Actual devices (iPhone, Android)
- Slow connections: Chrome DevTools throttling
Throttle Connection
Chrome DevTools:
- Network tab
- Throttling dropdown
- Select "Slow 3G"
- Reload page
- Watch for shifts
9. Use CSS Containment
Tell browser which elements won't affect others:
.ecwid-container {
contain: layout style paint;
/* Element's layout won't affect outside elements */
}
.product-card {
contain: layout;
/* Product card layout is self-contained */
}
Note: Use carefully, test thoroughly.
10. Implement Placeholder Content
Show placeholder while Ecwid loads:
<div id="my-store-123456">
<div class="ecwid-placeholder" style="min-height: 800px; display: flex; align-items: center; justify-content: center;">
<div style="text-align: center;">
<div class="spinner"></div>
<p>Loading store...</p>
</div>
</div>
</div>
<style>
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Hide placeholder when Ecwid loads */
.ecwid .ecwid-placeholder {
display: none;
}
</style>
<script>
Ecwid.OnPageLoad.add(function() {
document.querySelector('.ecwid-placeholder')?.remove();
});
</script>
Platform-Specific Fixes
WordPress + Ecwid
Common issues:
Solutions:
- Set widget container dimensions:
.ecwid-widget-container {
min-height: 800px;
}
Use page builder constraints:
- Elementor: Set min-height on section
- Gutenberg: Use Spacer blocks
Defer non-critical plugins:
// functions.php - defer non-essential plugins on Ecwid pages
add_action('wp_enqueue_scripts', function() {
if (is_page('store')) {
// Dequeue non-essential scripts
wp_dequeue_script('non-essential-plugin');
}
}, 100);
Wix + Ecwid
Challenges:
- Less control over Wix's rendering
- Wix's own shifts
Solutions:
- Use Wix Blocks with fixed heights
- Minimize Wix elements around Ecwid
- Use Wix's responsive settings
Squarespace + Ecwid
Optimizations:
- Use Code Block with min-height
- Disable auto-injected features
- Test in all Squarespace breakpoints
Debugging CLS Issues
Identify Shifting Elements
Chrome DevTools:
- Performance tab
- Record page load
- Look for "Layout Shift" entries
- Click to see shifted elements
Log Layout Shifts
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
console.log('Layout shift:', entry.value);
console.log('Shifted elements:', entry.sources);
console.log('Running CLS:', cls);
}
}
}).observe({type: 'layout-shift', buffered: true});
Add to Custom JavaScript Code section for debugging.
Track CLS in GA4
import {getCLS} from 'web-vitals';
getCLS(function(metric) {
gtag('event', 'CLS', {
value: Math.round(metric.value * 1000),
metric_value: metric.value,
metric_delta: metric.delta,
event_category: 'Web Vitals',
non_interaction: true
});
});
Testing After Fixes
Verify with PageSpeed Insights
- Clear cache
- Test in incognito
- Run PageSpeed Insights
- Check CLS score
- Verify improvements
Field Data
Check real user data:
- Google Search Console
- Experience → Core Web Vitals
- Check "CLS" tab
- Monitor over 28 days
A/B Test
Compare before/after:
- Control: Current implementation
- Test: With CLS fixes
- Monitor: Real user CLS scores
Common Issues
CLS Still High After Optimization
Possible causes:
- Ads or third-party content - Remove or reserve space
- Unoptimized images - Add dimensions to all images
- Custom fonts - Use font-display: optional
- Dynamic content - Reserve space before loading
CLS Worse on Mobile
Mobile-specific issues:
- Smaller viewport = shifts more noticeable
- Slower loading = more opportunity for shifts
- Touch interactions can trigger shifts
Solutions:
- Prioritize mobile testing
- Use mobile-specific min-heights
- Test on actual devices
Good Score in Lab, Poor in Field
Lab vs Field differences:
- Lab uses fast connection
- Field includes real users (slow connections, old devices)
- Field includes user interactions
Solutions:
- Test with throttling
- Test on old devices
- Monitor field data regularly
Best Practices
1. Set Dimensions for All Media
Images, videos, iframes - always specify dimensions:
<img src="image.jpg" width="500" height="300" alt="...">
<iframe src="..." width="560" height="315"></iframe>
2. Reserve Space for Dynamic Content
Anything that loads later needs reserved space.
3. Avoid Inserting Content Above Existing
Don't inject content that pushes down existing content.
4. Use Animations for Position Changes
If elements must move, animate with transform:
/* Good: doesn't cause layout shift */
.element {
transform: translateY(100px);
}
/* Bad: causes layout shift */
.element {
margin-top: 100px;
}
5. Preload Critical Resources
Fonts, hero images, above-fold content.
Monitoring & Maintenance
Regular Audits
Weekly: Check PageSpeed Insights Monthly: Review Search Console Core Web Vitals Quarterly: Deep audit of all third-party scripts
Track Changes
Before adding new features/scripts, check CLS impact:
- Baseline: Current CLS score
- Add feature
- Re-test: New CLS score
- Compare: Regression?
Next Steps
- Optimize LCP - Improve Largest Contentful Paint
- Events Not Firing - Debug tracking issues
For general performance concepts, see Core Web Vitals Guide.