| name | eds-performance-debugging |
| description | Guide for debugging and performance optimization of EDS blocks including error handling, FOUC prevention, Core Web Vitals optimization, and debugging workflows for Adobe Edge Delivery Services. |
EDS Performance & Debugging Guide
Purpose
Guide developers through debugging EDS blocks, optimizing performance, implementing proper error handling, and achieving excellent Core Web Vitals scores.
When to Use This Skill
Automatically activates when:
- Debugging errors in blocks or scripts
- Working with keywords: "error", "debug", "performance", "slow", "FOUC"
- Optimizing Core Web Vitals
- Handling exceptions in block code
Error Handling Patterns
Basic Error Handling in Blocks
export default function decorate(block) {
try {
// Extract content
const content = extractContent(block);
// Validate content
if (!content || content.length === 0) {
throw new Error('No content available');
}
// Create and render
const container = createStructure(content);
block.textContent = '';
block.appendChild(container);
} catch (error) {
// Log error to console
console.error('Block decoration failed:', error);
// Show user-friendly error
block.innerHTML = `
<div class="error-message">
<p>Unable to load content</p>
</div>
`;
}
}
Async Error Handling
export default async function decorate(block) {
try {
// Show loading state
showLoadingState(block);
// Fetch data
const response = await fetch('/api/data');
// Check response
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Validate data
if (!data || !Array.isArray(data)) {
throw new Error('Invalid data format');
}
// Render content
hideLoadingState(block);
renderContent(block, data);
} catch (error) {
console.error('Failed to load block data:', error);
// Show error state
hideLoadingState(block);
showErrorState(block, 'Failed to load content. Please try again later.');
}
}
function showLoadingState(block) {
block.innerHTML = '<div class="loading">Loading...</div>';
}
function hideLoadingState(block) {
const loading = block.querySelector('.loading');
if (loading) loading.remove();
}
function showErrorState(block, message) {
block.innerHTML = `
<div class="error-state">
<p>${message}</p>
<button onclick="location.reload()">Retry</button>
</div>
`;
}
Graceful Degradation
export default function decorate(block) {
try {
// Try to use modern feature
if ('IntersectionObserver' in window) {
setupLazyLoading(block);
} else {
// Fallback for older browsers
loadAllImagesImmediately(block);
}
} catch (error) {
console.error('Feature failed, using fallback:', error);
// Fallback implementation
basicImplementation(block);
}
}
Debugging Workflows
Console Logging Best Practices
export default function decorate(block) {
// Use console groups for organization
console.group('Block: your-block');
// Log input state
console.log('Input HTML:', block.innerHTML);
console.log('Block classes:', block.className);
try {
const content = extractContent(block);
console.log('Extracted content:', content);
const container = createStructure(content);
console.log('Created structure:', container);
block.textContent = '';
block.appendChild(container);
console.log('Final HTML:', block.innerHTML);
} catch (error) {
console.error('Decoration failed:', error);
console.trace(); // Show stack trace
}
console.groupEnd();
}
Conditional Debugging
const DEBUG = window.location.hostname === 'localhost';
function debug(...args) {
if (DEBUG) {
console.log('[DEBUG]', ...args);
}
}
export default function decorate(block) {
debug('Decorating block:', block.className);
const content = extractContent(block);
debug('Content extracted:', content);
renderContent(block, content);
debug('Rendering complete');
}
Performance Timing
export default async function decorate(block) {
const startTime = performance.now();
try {
await loadAndRender(block);
const endTime = performance.now();
const duration = endTime - startTime;
if (duration > 100) {
console.warn(`Block took ${duration.toFixed(2)}ms (target: <100ms)`);
} else {
console.log(`Block loaded in ${duration.toFixed(2)}ms`);
}
} catch (error) {
console.error('Block loading failed:', error);
}
}
FOUC Prevention
CSS-First Approach
/* your-block.css */
/* Hide block until decorated */
.your-block:not(.decorated) {
visibility: hidden;
}
/* Show block after decoration */
.your-block.decorated {
visibility: visible;
}
// your-block.js
export default function decorate(block) {
// Extract and process content
const content = extractContent(block);
const container = createStructure(content);
// Replace content
block.textContent = '';
block.appendChild(container);
// Mark as decorated (makes visible)
block.classList.add('decorated');
}
Placeholder Content
.your-block::before {
content: '';
display: block;
width: 100%;
height: 300px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
.your-block.decorated::before {
display: none;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Progressive Enhancement
export default function decorate(block) {
// Add loading class immediately
block.classList.add('loading');
try {
// Build enhanced version
const content = extractContent(block);
const enhanced = createEnhancedStructure(content);
// Replace with enhanced version
block.textContent = '';
block.appendChild(enhanced);
} catch (error) {
console.error('Enhancement failed:', error);
// Keep original content if enhancement fails
} finally {
// Remove loading class
block.classList.remove('loading');
block.classList.add('decorated');
}
}
Core Web Vitals Optimization
Largest Contentful Paint (LCP)
export default function decorate(block) {
// Prioritize above-the-fold images
const images = block.querySelectorAll('img');
images.forEach((img, index) => {
if (index === 0) {
// First image: high priority, no lazy loading
img.loading = 'eager';
img.fetchpriority = 'high';
} else {
// Other images: lazy load
img.loading = 'lazy';
}
});
}
Cumulative Layout Shift (CLS)
/* Reserve space for images to prevent layout shift */
.your-block img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* Reserve space */
}
/* Reserve space for dynamic content */
.your-block-container {
min-height: 300px; /* Prevent shift during loading */
}
export default async function decorate(block) {
// Set explicit dimensions before loading content
const height = block.offsetHeight;
block.style.minHeight = `${height}px`;
// Load content
await loadContent(block);
// Remove min-height after content loaded
block.style.minHeight = '';
}
First Input Delay (FID)
export default function decorate(block) {
// Defer non-critical work
const criticalSetup = () => {
// Critical rendering
const content = extractContent(block);
renderContent(block, content);
};
const nonCriticalSetup = () => {
// Analytics, animations, etc.
setupAnalytics(block);
setupAnimations(block);
};
// Run critical work immediately
criticalSetup();
// Defer non-critical work
if ('requestIdleCallback' in window) {
requestIdleCallback(nonCriticalSetup);
} else {
setTimeout(nonCriticalSetup, 1);
}
}
Performance Optimization Patterns
Minimize DOM Manipulation
// ❌ BAD - Multiple reflows
export default function decorate(block) {
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
block.appendChild(div); // Reflow on each append
});
}
// ✅ GOOD - Single reflow
export default function decorate(block) {
const fragment = document.createDocumentFragment();
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
fragment.appendChild(div);
});
block.textContent = '';
block.appendChild(fragment); // Single reflow
}
Debounce Expensive Operations
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
export default function decorate(block) {
const handleResize = debounce(() => {
// Expensive resize logic
recalculateLayout(block);
}, 250);
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}
Lazy Load Images
export default function decorate(block) {
const images = block.querySelectorAll('img');
images.forEach(img => {
// Native lazy loading
img.loading = 'lazy';
// Or use Intersection Observer
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const image = entry.target;
image.src = image.dataset.src;
observer.unobserve(image);
}
});
});
observer.observe(img);
}
});
}
Optimize Event Listeners
export default function decorate(block) {
// ❌ BAD - Multiple listeners
const items = block.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// ✅ GOOD - Event delegation
block.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
handleClick(e, item);
}
});
}
Common Issues and Solutions
Issue: Block Not Rendering
Check:
export default function decorate(block) {
// 1. Check if block exists
if (!block) {
console.error('Block is null or undefined');
return;
}
// 2. Check if block has content
console.log('Block HTML:', block.innerHTML);
if (!block.children.length) {
console.warn('Block has no children');
}
// 3. Check for JavaScript errors
try {
const content = extractContent(block);
console.log('Extracted content:', content);
} catch (error) {
console.error('Content extraction failed:', error);
}
}
Issue: CSS Not Loading
Solution:
Verify file names match exactly:
blocks/your-block/ ├── your-block.js ← Must match ├── your-block.css ← Must matchCheck browser DevTools → Network tab for 404 errors
Ensure CSS is valid:
npm run lint:css
Issue: Memory Leaks
Solution: Clean up event listeners
export default function decorate(block) {
const handleClick = () => {
console.log('Clicked');
};
// Add listener
block.addEventListener('click', handleClick);
// Return cleanup function
return () => {
block.removeEventListener('click', handleClick);
};
}
Issue: Race Conditions
Solution: Use proper async/await
// ❌ BAD - Race condition
export default function decorate(block) {
fetch('/api/data')
.then(r => r.json())
.then(data => renderData(block, data));
// This runs before fetch completes
setupEventListeners(block);
}
// ✅ GOOD - Proper sequencing
export default async function decorate(block) {
const response = await fetch('/api/data');
const data = await response.json();
renderData(block, data);
// This runs after data is rendered
setupEventListeners(block);
}
Browser DevTools Tips
Network Tab
Monitor:
- CSS file loading (should be automatic)
- JavaScript module loading
- API requests and responses
- Failed requests (404, 500)
Console Tab
Use:
console.log()for debuggingconsole.error()for errorsconsole.warn()for warningsconsole.table()for structured dataconsole.time()/console.timeEnd()for timing
Performance Tab
Record and analyze:
- Start recording
- Interact with your block
- Stop recording
- Review:
- Scripting time
- Rendering time
- Painting time
- Long tasks (>50ms)
Elements Tab
Use:
- Inspect DOM structure
- Check computed styles
- View event listeners
- Check accessibility tree
Performance Monitoring
Custom Timing
export default async function decorate(block) {
// Mark start
performance.mark('block-start');
// Do work
await loadAndRender(block);
// Mark end
performance.mark('block-end');
// Measure
performance.measure('block-time', 'block-start', 'block-end');
// Get results
const measures = performance.getEntriesByName('block-time');
console.log(`Block took ${measures[0].duration.toFixed(2)}ms`);
// Clean up
performance.clearMarks();
performance.clearMeasures();
}
Memory Usage
if (performance.memory) {
console.log('Memory usage:', {
used: (performance.memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB',
total: (performance.memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB',
limit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB'
});
}
Testing for Performance
Load Time Testing
// Add to test.html
<script type="module">
import { loadBlock } from '/scripts/aem.js';
const block = document.querySelector('.your-block');
const startTime = performance.now();
await loadBlock(block);
const endTime = performance.now();
const duration = endTime - startTime;
console.log(`Load time: ${duration.toFixed(2)}ms`);
if (duration > 100) {
console.warn('⚠️ Load time exceeds 100ms target');
} else {
console.log('✅ Load time within target');
}
</script>
Lighthouse Testing
# Test your block locally
npm run debug
# In Chrome:
# 1. Open DevTools
# 2. Go to Lighthouse tab
# 3. Select categories: Performance, Accessibility
# 4. Click "Generate report"
Target scores:
- Performance: 90+
- Accessibility: 90+
- Best Practices: 90+
Error Boundaries
Global Error Handler
// In scripts/scripts.js or similar
window.addEventListener('error', (event) => {
console.error('Global error:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error
});
// Optionally send to analytics
// trackError(event.error);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Optionally send to analytics
// trackError(event.reason);
});
Block-Specific Error Boundary
export default function decorate(block) {
// Wrap entire block in try-catch
const originalDecorate = () => {
const content = extractContent(block);
const container = createStructure(content);
block.textContent = '';
block.appendChild(container);
};
try {
originalDecorate();
} catch (error) {
console.error(`Block ${block.className} failed:`, error);
// Show error UI
block.innerHTML = `
<div class="block-error">
<p>This content is temporarily unavailable.</p>
${DEBUG ? `<pre>${error.stack}</pre>` : ''}
</div>
`;
}
}
Related Documentation
- EDS Block Development - Development patterns
- EDS Block Testing - Testing workflows
- Debug Guide - Comprehensive debugging
- Instrumentation - Performance monitoring
Performance Checklist
Before deploying your block:
- No JavaScript errors in console
- Load time < 100ms for simple blocks
- No layout shifts (CLS score < 0.1)
- Images lazy loaded (except hero images)
- Event delegation used for multiple items
- Proper error handling implemented
- Memory leaks checked and fixed
- Tested on slow network (3G)
- Tested on mobile devices
- Lighthouse performance score 90+
Next Steps
- Review your block for potential performance issues
- Add proper error handling with try-catch
- Implement FOUC prevention with CSS
- Test performance with DevTools
- Run Lighthouse audit
- Fix any issues identified
- Document performance characteristics
Remember: Fast, reliable blocks create better user experiences and improve your site's Core Web Vitals scores!