| 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. Use when this capability is needed. |
| metadata | {"author":"ddttom"} |
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 {
const content = extractContent(block);
if (!content || content.length === 0) {
throw new Error('No content available');
}
const container = createStructure(content);
block.textContent = '';
block.appendChild(container);
} catch (error) {
console.error('Block decoration failed:', error);
block.innerHTML = `
<div class="error-message">
<p>Unable to load content</p>
</div>
`;
}
}
Async Error Handling
export default async function decorate(block) {
try {
showLoadingState(block);
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (!data || !Array.isArray(data)) {
throw new Error('Invalid data format');
}
hideLoadingState(block);
renderContent(block, data);
} catch (error) {
console.error('Failed to load block data:', error);
hideLoadingState(block);
showErrorState(block, 'Failed to load content. Please try again later.');
}
}
function showLoadingState(block) {
block. = ;
}
() {
loading = block.();
(loading) loading.();
}
() {
block. = ;
}
Graceful Degradation
export default function decorate(block) {
try {
if ('IntersectionObserver' in window) {
setupLazyLoading(block);
} else {
loadAllImagesImmediately(block);
}
} catch (error) {
console.error('Feature failed, using fallback:', error);
basicImplementation(block);
}
}
Debugging Workflows
Console Logging Best Practices
export default function decorate(block) {
console.group('Block: your-block');
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();
}
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:not(.decorated) {
visibility: hidden;
}
.your-block.decorated {
visibility: visible;
}
export default function decorate(block) {
const content = extractContent(block);
const container = createStructure(content);
block.textContent = '';
block.appendChild(container);
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) {
block.classList.add('loading');
try {
const content = extractContent(block);
const enhanced = createEnhancedStructure(content);
block.textContent = '';
block.appendChild(enhanced);
} catch (error) {
console.error('Enhancement failed:', error);
} finally {
block.classList.remove('loading');
block.classList.add('decorated');
}
}
Core Web Vitals Optimization
Largest Contentful Paint (LCP)
export default function decorate(block) {
const images = block.querySelectorAll('img');
images.forEach((img, index) => {
if (index === 0) {
img.loading = 'eager';
img.fetchpriority = 'high';
} else {
img.loading = 'lazy';
}
});
}
Cumulative Layout Shift (CLS)
.your-block img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
.your-block-container {
min-height: 300px;
}
export default async function decorate(block) {
const height = block.offsetHeight;
block.style.minHeight = `${height}px`;
await loadContent(block);
block.style.minHeight = '';
}
First Input Delay (FID)
export default function decorate(block) {
const criticalSetup = () => {
const content = extractContent(block);
renderContent(block, content);
};
const nonCriticalSetup = () => {
setupAnalytics(block);
setupAnimations(block);
};
criticalSetup();
if ('requestIdleCallback' in window) {
requestIdleCallback(nonCriticalSetup);
} else {
setTimeout(nonCriticalSetup, 1);
}
}
Performance Optimization Patterns
Minimize DOM Manipulation
export default function decorate(block) {
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
block.appendChild(div);
});
}
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);
}
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(() => {
recalculateLayout(block);
}, 250);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}
Lazy Load Images
export default function decorate(block) {
const images = block.querySelectorAll('img');
images.forEach(img => {
img.loading = 'lazy';
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) {
const items = block.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
block.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
handleClick(e, item);
}
});
}
Common Issues and Solutions
Issue: Blank Page / Block Not Visible
Symptoms:
- Console shows no errors
- JavaScript executes successfully
- Elements are in the DOM
- But page appears completely blank
Root Cause: EDS global styles hide <body> by default
Solution:
EDS uses a visibility pattern where the body is hidden until content is ready:
body {
display: none;
}
body.appear {
display: block;
}
Fix for test files:
<script type="module">
import decorate from './your-block.js';
document.body.classList.add('appear');
document.addEventListener('DOMContentLoaded', () => {
const blocks = document.querySelectorAll('.your-block');
blocks.forEach(decorate);
});
</script>
Debugging Steps:
-
Check if elements exist:
console.log('Button in DOM:', document.querySelector('.your-button'));
-
Check computed styles:
- Open DevTools → Elements tab
- Inspect the element
- Check Computed styles for
display: none or visibility: hidden
-
Check body visibility:
console.log('Body classes:', document.body.className);
console.log('Body computed display:', getComputedStyle(document.body).display);
-
Force visibility (debugging):
document.body.classList.add('appear');
Production Notes:
- In production, EDS automatically adds
appear class when page loads
- Test files need to add it manually
- This pattern prevents FOUC (Flash of Unstyled Content)
Issue: Block Not Rendering
Check:
export default function decorate(block) {
if (!block) {
console.error('Block is null or undefined');
return;
}
console.log('Block HTML:', block.innerHTML);
if (!block.children.length) {
console.warn('Block has no children');
}
try {
const content = extractContent(block);
console.log('Extracted content:', content);
} catch (error) {
console.error('Content extraction failed:', error);
}
}
Issue: Buttons Not Styled
Symptoms:
- Buttons exist in DOM but look unstyled
- No background color, borders, or padding
Root Cause: Global button styles not loading or not being applied
Solution:
-
Verify global styles load:
<link rel="stylesheet" href="/styles/styles.css">
-
Check button inherits global styles:
button {
display: inline-block;
padding: 5px 30px;
background-color: var(--link-color);
color: var(--background-color);
border-radius: 30px;
}
-
Add fallback styles if needed:
.your-block button {
display: inline-block;
padding: 10px 30px;
background-color: #0066cc;
color: white;
border: none;
border-radius: 30px;
cursor: pointer;
}
Best Practice: Rely on global styles, only add block-specific overrides
Issue: CSS Not Loading
Solution:
-
Verify file names match exactly:
blocks/your-block/
├── your-block.js ← Must match
├── your-block.css ← Must match
-
Check 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');
};
block.addEventListener('click', handleClick);
return () => {
block.removeEventListener('click', handleClick);
};
}
Issue: Race Conditions
Solution: Use proper async/await
export default function decorate(block) {
fetch('/api/data')
.then(r => r.json())
.then(data => renderData(block, data));
setupEventListeners(block);
}
export default async function decorate(block) {
const response = await fetch('/api/data');
const data = await response.json();
renderData(block, data);
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 debugging
console.error() for errors
console.warn() for warnings
console.table() for structured data
console.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) {
performance.mark('block-start');
await loadAndRender(block);
performance.mark('block-end');
performance.measure('block-time', 'block-start', 'block-end');
const measures = performance.getEntriesByName('block-time');
console.log(`Block took ${measures[0].duration.toFixed(2)}ms`);
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
<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
npm run debug
Target scores:
- Performance: 90+
- Accessibility: 90+
- Best Practices: 90+
Error Boundaries
Global Error Handler
window.addEventListener('error', (event) => {
console.error('Global error:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error
});
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
});
Block-Specific Error Boundary
export default function decorate(block) {
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);
block.innerHTML = `
<div class="block-error">
<p>This content is temporarily unavailable.</p>
${DEBUG ? `<pre>${error.stack}</pre>` : ''}
</div>
`;
}
}
Related Documentation
Performance Checklist
Before deploying your block:
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!
Converted and distributed by TomeVault — claim your Tome and manage your conversions.