plugins/aem/edge-delivery-services-content-ops/skills/cwv-optimizer/SKILL.md
Diagnose and fix Core Web Vitals issues on AEM Edge Delivery Services pages. Goes deeper than generic CWV advice by understanding EDS-specific performance patterns including the 100KB LCP budget, E-L-D loading phases, block rendering behavior, and third-party script impact. Produces specific fixes for LCP, CLS, and INP issues with before/after projections. Use when the user asks about Core Web Vitals, page speed, or performance issues on AEM Edge Delivery Services (EDS/Franklin) sites.
npx skillsauth add adobe/skills cwv-optimizerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Diagnose and fix Core Web Vitals issues on AEM Edge Delivery Services pages using EDS-specific domain knowledge: the 100KB LCP budget, the Eager-Lazy-Delayed loading phases, block architecture, the createOptimizedPicture() function, and the /scripts/delayed.js pattern. Produces specific, implementable fixes with estimated impact projections, not generic performance advice.
This skill fetches external web pages for analysis. When fetching:
optel-interpreter first), non-EDS sites, or server-side TTFB/CDN issues.Before starting, create a checklist of all steps to track progress:
Fetch the page and collect baseline scores:
curl -s -o /dev/null -w "HTTP %{http_code} - %{size_download} bytes - %{time_total}s" "https://<domain>/<path>"
Record baseline CWV values, total page weight, request count, and TTFB. A large FCP-to-LCP gap suggests render-blocking resources between first paint and largest paint.
Identify the LCP element from measured data, not from page structure. Use Chrome DevTools (Performance panel → the LCP marker, or the Lighthouse "Largest Contentful Paint element" audit) or RUM field data. In EDS the LCP element is commonly the first image or a large <h1> in the first section, but confirm it rather than assuming. Once confirmed, fetch the HTML and examine that element in the first section (before the first --- divider).
Inventory every eager-phase resource and measure actual transfer sizes. Build the budget table: HTML document, /styles/styles.css, /scripts/aem.js, /scripts/scripts.js, first-section block CSS/JS, preloaded fonts, and LCP image. Grade the total against the 100KB budget (see references/cwv-eds-reference.md for grading scale).
Use RUM field data to see real-user LCP for the page, and process it with Adobe's official @adobe/rum-distiller library (the same one the OpTel Explorer uses) rather than hand-parsing checkpoint events:
import { DataChunks, series, utils } from '@adobe/rum-distiller';
// The bundle API is path-based: https://bundles.aem.page/bundles/{domain}/{year}/{month}/{day}.
// The domain key is the ?domainkey= query parameter, not an Authorization header.
const resp = await fetch(
`https://bundles.aem.page/bundles/example.com/2026/06/28?domainkey=${RUM_DOMAIN_KEY}`,
);
const { rumBundles } = await resp.json();
// addCalculatedProps derives the cwvLCP/cwvCLS/cwvINP props that the series read.
rumBundles.forEach((b) => utils.addCalculatedProps(b));
const dc = new DataChunks();
dc.load([{ date: '2026-06-28', rumBundles }]);
dc.addSeries('lcp', series.lcp);
console.log(`p75 LCP: ${dc.totals.lcp.percentile(75)}ms`);
Verify resources load in the correct phase:
Eager: Only first-section block CSS/JS. Check that below-fold blocks are not loading eagerly. Images in the first section must have loading="eager" with width and height; below-fold images must have loading="lazy".
Delayed: Fetch /scripts/delayed.js and verify all third-party scripts load there. Common violations: Google Tag Manager in <head> (~70KB, blocks render), analytics loaded synchronously, chat widgets loaded eagerly, consent banners in the eager phase.
Fonts: Verify font-display: swap, maximum 2 preloaded fonts, all WOFF2 format, each under 30KB. Fonts used only below the fold should not be preloaded.
Check whether images have explicit width and height:
curl -s "https://<domain>/<path>" | grep -oP '<img[^>]*>' | head -10
Images without dimensions cause CLS. The createOptimizedPicture() function in aem.js does not set width/height attributes on the images it generates. Fix by adding the attributes in the block's decorate() function.
EDS automatically serves content images as responsive WebP through its <picture> pipeline (the ?width=…&format=webply&optimize=medium transform), regardless of the source format, so do not tell the agent to convert content images to WebP/AVIF or resize them by hand. The lever you actually control is the source image: an oversized original (e.g. 4000px wide) inflates the delivered derivatives. Check the delivered LCP image's transfer size in the network waterfall; if it is heavy, reduce the source image's intrinsic dimensions or crop it, not its format. Only images bundled in code (block icons/SVG) are optimized by you directly, so keep those small and prefer inline SVG. (EDS delivers WebP, not AVIF.)
EDS CLS comes from a predictable set of sources:
width/height from createOptimizedPicture().font-display: swap without size-adjust and ascent-override on the fallback @font-face.decorate(). Reserve space with CSS or make initial HTML match final layout.Look for long tasks (> 50ms), forced reflows, and slow event handlers (> 100ms). Common EDS offenders: carousel blocks recalculating all slide layouts, accordion/tab blocks triggering full reflows instead of CSS transitions, mega-menus injecting large DOM subtrees synchronously, and search blocks filtering on every keystroke without debouncing.
Measure LCP element render timing in a block's decorate() function:
// Measure LCP contribution from a block
export default async function decorate(block) {
const start = performance.now();
// ... block decoration logic ...
const elapsed = performance.now() - start;
if (elapsed > 50) {
console.warn(`[perf] ${block.dataset.blockName} decorate took ${elapsed.toFixed(1)}ms (budget: 50ms)`);
}
}
Key fixes: debounce expensive handlers (150ms), batch DOM reads before writes to avoid forced reflows, use requestAnimationFrame for visual updates.
Inventory all external scripts from the HTML head and delayed.js:
curl -s "https://<domain>/<path>" | grep -oP '<script[^>]*src="[^"]*"' | head -20
curl -s "https://<domain>/scripts/delayed.js"
Classify each script by current phase vs. correct phase. All third-party scripts must load via delayed.js (3+ seconds after page load). Scripts in the eager phase add directly to the 100KB budget. If GTM re-injects scripts dynamically, configure GTM triggers to fire only after a 3-second delay.
For each issue, produce a specific fix with estimated impact:
| Issue | Metric | Current | Fix | Projected After | |-------|--------|---------|-----|-----------------| | Hero image 180KB | LCP | 3.2s | Reduce source image dimensions / crop (EDS already serves WebP) | 2.1s | | GTM in head | LCP | 3.2s | Move to delayed.js | 2.4s | | Images missing dimensions | CLS | 0.18 | Add width/height to createOptimizedPicture | 0.03 | | Font swap without size-adjust | CLS | 0.18 | Add size-adjust to fallback | 0.08 | | Carousel forced reflow | INP | 310ms | Batch DOM reads/writes | 150ms |
See references/cwv-eds-reference.md for projection benchmarks (image format savings, reflow reduction estimates).
| Metric | Before | Target | Projected After | Status | |--------|--------|--------|-----------------|--------| | LCP | X.Xs | < 2.5s | X.Xs | Fix/Monitor/Pass | | CLS | X.XX | < 0.1 | X.XX | Fix/Monitor/Pass | | INP | Xms | < 200ms | Xms | Fix/Monitor/Pass |
Ranked list: highest-impact fix first, with metric affected, estimated improvement, and effort.
loading="eager" with width and heightloading="lazy"delayed.jsfont-display: swap with size-adjust fallbackstools
Use the run-workflow MCP to discover, compose, execute, publish, and save Adobe Firefly workflows. TRIGGER when: user asks what actions are available, what the MCP can do, how to process images/video/3D via workflow, wants to build/run/save/publish a workflow, OR pastes any workflow/batch/execution ID. BARE ID (UUID/workflowId/batchId) = INSPECT ONLY — call inspect_run, NEVER run_workflow_submit. ALWAYS call list_actions first for capability/discovery questions. DO NOT TRIGGER for direct Firefly API calls without MCP (use firefly-api-specs).
tools
Run predefined featured workflows via run-workflow MCP. TRIGGER when user names a featured workflow (retargeting, banners at scale, localization, packaging, banner advertising, etc.) or asks to run a known marketing/production workflow. Requires run-workflow MCP. ALWAYS call get_featured_workflow before compose_workflow. DO NOT TRIGGER for custom one-off workflows with no named template — use run-workflow skill.
tools
Migrate an Adobe Commerce App Builder project from the Integration Starter Kit or Checkout Starter Kit to the new App Management approach. Run from the root of the App Builder project to be migrated. Pass --auto to skip confirmation prompts (suitable for CI or batch use) — auto mode prints a summary of all Q&A questions answered with their defaults. Pass --doc-scan-only to scan README.md and env.dist for outdated content without modifying any files. Use when the user wants to migrate an App Builder project from the Integration Starter Kit or Checkout Starter Kit to the App Management approach, or mentions upgrading their Adobe Commerce extension architecture.
development
Add or modify webhook interceptors in an Adobe Commerce app. Use when the user wants to intercept Commerce operations to validate input, append data, or modify behavior — before or after execution. Requires a base app initialized with commerce-app-init.