Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Frontend performance optimization expertise covering Core Web Vitals, bundle optimization, code splitting, lazy loading, image and font optimization, caching strategies, service workers, and performance monitoring. Use when the user asks about web performance, web performance best practices, or needs guidance on web performance implementation. Do NOT use when the user needs a different specialized skill or is asking about an unrelated technology domain.
.claude/skills/ferroxlabs-web-performance/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 86% | 30 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-17 | ✗→✓ | ▲ Improved | 336% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 220% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 282% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 379% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 204% | 0% |
Optimize web application performance across all dimensions -- loading speed, interactivity, visual stability, and runtime efficiency. This skill provides actionable strategies for achieving excellent Core Web Vitals scores and maintaining performance budgets.
LCP (Largest Contentful Paint) -- Loading Performance
Good: <= 2.5s
Needs Improvement: 2.5s - 4.0s
Poor: > 4.0s
What it measures: Time until the largest content element is rendered
Common LCP elements: hero images, heading text, video posters
FID (First Input Delay) -- Interactivity (being replaced by INP)
Good: <= 100ms
Needs Improvement: 100ms - 300ms
Poor: > 300ms
INP (Interaction to Next Paint) -- Responsiveness
Good: <= 200ms
Needs Improvement: 200ms - 500ms
Poor: > 500ms
What it measures: Latency of all user interactions throughout page lifecycle
CLS (Cumulative Layout Shift) -- Visual Stability
Good: <= 0.1
Needs Improvement: 0.1 - 0.25
Poor: > 0.25
What it measures: Sum of unexpected layout shift scoresSTEP 1: Identify the LCP element
-> Chrome DevTools > Performance > timings > LCP marker
-> Usually: <img>, <video>, <h1>, or CSS background-image
STEP 2: Optimize based on element type
If LCP is an IMAGE:
[ ] Add fetchpriority="high" to the <img> tag
[ ] Preload the image: <link rel="preload" as="image" href="...">
[ ] Use modern formats (WebP/AVIF with fallback)
[ ] Use responsive srcset with appropriate sizes
[ ] Serve from CDN with appropriate cache headers
[ ] Remove lazy loading from LCP image (never lazy-load above the fold)
If LCP is TEXT:
[ ] Preload critical fonts: <link rel="preload" as="font" crossorigin>
[ ] Use font-display: optional or swap
[ ] Inline critical CSS
[ ] Reduce render-blocking resources
If LCP is a CSS BACKGROUND:
[ ] Convert to <img> tag if possible
[ ] Preload the background image
[ ] Inline the critical CSS that defines the background
STEP 3: Reduce TTFB (Time to First Byte)
[ ] Use CDN for static assets
[ ] Enable compression (Brotli > gzip)
[ ] Optimize server response time
[ ] Use HTTP/2 or HTTP/3
[ ] Consider stale-while-revalidate cachingSTEP 1: Identify slow interactions
-> Chrome DevTools > Performance > Interactions track
-> PerformanceObserver with type: 'event'
STEP 2: Reduce input delay
[ ] Minimize main thread work during page load
[ ] Break up long tasks (> 50ms) using scheduler.yield()
[ ] Defer non-critical JavaScript with defer/async
[ ] Remove unnecessary event listeners
STEP 3: Reduce processing time
[ ] Optimize event handler logic
[ ] Move expensive computation to Web Workers
[ ] Use requestAnimationFrame for visual updates
[ ] Debounce/throttle rapid-fire events (scroll, resize, input)
STEP 4: Reduce presentation delay
[ ] Minimize DOM size (< 1500 nodes ideal)
[ ] Reduce CSS complexity (avoid expensive selectors)
[ ] Use content-visibility: auto for offscreen content
[ ] Avoid forced synchronous layouts (read then write, never interleave)Common CLS causes and fixes:
1. Images without dimensions:
<img src="hero.jpg" width="800" height="600" alt="..."> ./* Or use aspect-ratio in CSS */
img { aspect-ratio: 16 / 9; width: 100%; height: auto; }
2. Ads/embeds without reserved space:
.ad-slot { min-height: 250px; }
3. Web fonts causing layout shift:
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: optional; /* No FOUT, no layout shift */
size-adjust: 100.5%; /* Match fallback metrics */
}
4. Dynamic content inserted above viewport:
-> Insert below the fold or use CSS containment
-> Use transform animations instead of height/margin changes
5. Late-loading components:
-> Reserve space with skeleton placeholders
-> Use min-height on containersshell# Webpack npx webpack-bundle-analyzer stats.json # Vite npx vite-bundle-visualizer # Generic npx source-map-explorer dist/assets/*.js
RECOMMENDED BUDGETS:
Total JS (compressed): < 200KB
Main bundle: < 100KB
Per-route chunk: < 50KB
Third-party total: < 100KB
PACKAGE SIZE AWARENESS (gzipped):
react + react-dom: ~45KB
vue: ~33KB
lodash (full): ~25KB -> Use lodash-es with tree-shaking
moment.js: ~70KB -> Use date-fns (~7KB for used functions)
chart.js: ~60KB -> Lazy loadts// BAD: Imports entire library import _ from 'lodash'; _.debounce(fn, 300); // GOOD: Import specific function import debounce from 'lodash-es/debounce'; debounce(fn, 300); // BAD: Barrel file re-exports prevent tree shaking in some bundlers export * from './utils'; // GOOD: Named exports export { formatDate } from './date'; export { formatCurrency } from './currency'; // Ensure package.json enables tree shaking { "sideEffects": false, // or list specific files with side effects "module": "dist/index.esm.js" }
tsximport { lazy, Suspense } from 'react'; const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); const Analytics = lazy(() => import('./pages/Analytics')); function App() { return ( <Suspense fallback={<PageSkeleton />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/analytics" element={<Analytics />} /> </Routes> </Suspense> ); }
tsx// Heavy components loaded on demand const MarkdownEditor = lazy(() => import('./components/MarkdownEditor')); const PdfViewer = lazy(() => import('./components/PdfViewer')); const ChartWidget = lazy(() => import('./components/ChartWidget')); // Interaction-triggered loading function CommentSection() { const [showEditor, setShowEditor] = useState(false); return ( <div> <button onClick={() => setShowEditor(true)}>Write Comment</button> {showEditor && ( <Suspense fallback={<EditorSkeleton />}> <MarkdownEditor /> </Suspense> )} </div> ); }
html<!-- Prefetch likely-next routes --> <link rel="prefetch" href="/dashboard/chunk.js"> <!-- Preload critical current-route resources --> <link rel="preload" href="/hero.webp" as="image" fetchpriority="high"> <link rel="preload" href="/critical.css" as="style"> <link rel="preload" href="/font.woff2" as="font" crossorigin> <!-- DNS prefetch for external origins --> <link rel="dns-prefetch" href="[reference URL]"> <!-- Preconnect for critical external origins --> <link rel="preconnect" href="[reference URL]" crossorigin>
Photograph/complex imagery:
-> AVIF (best compression) with WebP fallback
-> Use <picture> element for format selection
-> Responsive srcset for resolution switching
Icons/logos/illustrations:
-> SVG (inline or sprite)
-> Optimize with SVGO
Simple graphics with transparency:
-> WebP or PNG (for legacy)
Animated content:
-> Short: CSS animation or Lottie
-> Longer: MP4 video (not GIF -- 10x larger)html<picture> <!-- AVIF for browsers that support it --> <source type="image/avif" srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" > <!-- WebP fallback --> <source type="image/webp" srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" > <!-- JPEG fallback --> <img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" alt="Hero image description" width="1200" height="600" loading="lazy" decoding="async" > </picture> <!-- Above-the-fold LCP image: NO lazy loading, YES fetchpriority --> <img src="hero.webp" alt="..." width="1200" height="600" fetchpriority="high" decoding="async">
css/* Optimal font-face declaration */ @font-face { font-family: 'Inter'; src: url('/fonts/inter-var.woff2') format('woff2'); font-weight: 100 900; font-display: swap; /* Show fallback immediately, swap when loaded */ unicode-range: U+0000-00FF, U+0131, U+0152-0153; /* Latin subset */ } /* Fallback font metrics matching to reduce CLS */ @font-face { font-family: 'Inter-fallback'; src: local('Arial'); size-adjust: 107%; ascent-supersede: 90%; descent-supersede: 22%; line-gap-supersede: 0%; } body { font-family: 'Inter', 'Inter-fallback', system-ui, sans-serif; }
Is this a system font?
YES -> font-family: system-ui, sans-serif (zero load time)
NO -> Is it critical for brand?
YES -> font-display: swap + preload WOFF2
NO -> font-display: optional (no FOUT, may not load on slow networks)Static assets (JS, CSS, images with hash in filename):
Cache-Control: public, max-age=31536000, immutable
HTML documents:
Cache-Control: no-cache
(Always revalidate with server, but can use 304)
API responses:
Cache-Control: private, max-age=0, must-revalidate
ETag: "abc123"
Fonts:
Cache-Control: public, max-age=31536000, immutable
(Fonts rarely change, hash in URL)js// Cache-first for static assets self.addEventListener('get', (event) => { if (event.request.destination === 'image' || event.request.destination === 'style' || event.request.destination === 'script') { event.respondWith( caches.match(event.request).then(cached => cached || get(event.request).then(response => { const clone = response.clone(); caches.open('static-v1').then(cache => cache.put(event.request, clone)); return response; }) ) ); } });
ts// Monitor LCP new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime); }).observe({ type: 'largest-contentful-paint', buffered: true }); // Monitor CLS let clsValue = 0; new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; } } console.log('CLS:', clsValue); }).observe({ type: 'layout-shift', buffered: true }); // Monitor INP new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.interactionId) { console.log('Interaction:', entry.name, entry.duration, 'ms'); } } }).observe({ type: 'event', buffered: true, durationThreshold: 16 }); // Monitor Long Tasks new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('Long Task:', entry.duration, 'ms'); } }).observe({ type: 'longtask' });
json// bundlesize or size-limit config { "budgets": [ { "path": "dist/js/main.*.js", "maxSize": "100KB" }, { "path": "dist/js/vendor.*.js", "maxSize": "150KB" }, { "path": "dist/css/*.css", "maxSize": "30KB" }, { "path": "dist/js/*.js", "maxSize": "300KB" } ] }
defer attributeUse this skill when:
Do NOT use this skill when:
markdown# Web Performance Analysis ## Context Assessment [Situation summary and constraints] ## Recommended Approach [Primary recommendation with rationale] ## Implementation Steps 1. [Step with specific details] 2. [Step with specific details] 3. [Step with specific details] ## Trade-offs and Considerations - [Key trade-off 1] - [Key trade-off 2] ## Next Steps - [Immediate action item] - [Follow-up action item]
Input: "Help me implement web performance for a medium-scale production application"
Output: A structured analysis covering current state assessment, recommended web performance approach with specific patterns, implementation roadmap with milestones, and risk mitigation strategies tailored to the application scale and constraints.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 95,108 | 51,284 | -46% | 1 | 1 | 0% | 5,821 | 7,698 | +32% | 0 | 0 | — |
case-02 | pass→pass | 12,658 | 15,841 | +25% | 1 | 1 | 0% | 2,220 | 7,106 | +220% | 0 | 0 | — |
case-03 | pass→pass | 14,473 | 13,174 | -9% | 1 | 1 | 0% | 1,702 | 6,495 | +282% | 0 | 0 | — |
case-04 | pass→pass | 8,063 | 15,020 | +86% | 1 | 1 | 0% | 1,234 | 5,914 | +379% | 0 | 0 | — |
case-05 | pass→pass | 27,259 | 16,833 | -38% | 1 | 1 | 0% | 2,322 | 7,058 | +204% | 0 | 0 | — |
case-06 | pass→pass | 14,898 | 14,198 | -5% | 1 | 1 | 0% | 2,210 | 6,698 | +203% | 0 | 0 | — |
case-07 | pass→pass | 13,829 | 22,105 | +60% | 1 | 1 | 0% | 2,078 | 7,026 | +238% | 0 | 0 | — |
case-08 | pass→pass | 19,196 | 15,355 | -20% | 1 | 1 | 0% | 2,100 | 6,407 | +205% | 0 | 0 | — |
case-09 | pass→pass | 10,884 | 15,464 | +42% | 1 | 1 | 0% | 1,745 | 6,723 | +285% | 0 | 0 | — |
case-10 | pass→pass | 5,926 | 9,335 | +58% | 1 | 1 | 0% | 898 | 5,775 | +543% | 0 | 0 | — |
case-11 | pass→pass | 15,088 | 17,112 | +13% | 1 | 1 | 0% | 2,360 | 6,978 | +196% | 0 | 0 | — |
case-12 | pass→pass | 18,756 | 16,553 | -12% | 1 | 1 | 0% | 2,755 | 7,351 | +167% | 0 | 0 | — |
case-13 | pass→pass | 7,283 | 11,264 | +55% | 1 | 1 | 0% | 1,005 | 5,971 | +494% | 0 | 0 | — |
case-14 | pass→pass | 8,563 | 12,203 | +43% | 1 | 1 | 0% | 1,307 | 6,063 | +364% | 0 | 0 | — |
case-15 | pass→pass | 5,706 | 5,294 | -7% | 1 | 1 | 0% | 605 | 5,064 | +737% | 0 | 0 | — |
case-16 | pass→pass | 4,395 | 4,847 | +10% | 1 | 1 | 0% | 623 | 4,972 | +698% | 0 | 0 | — |
case-17 | fail→pass | 7,307 | 9,521 | +30% | 1 | 1 | 0% | 1,345 | 5,866 | +336% | 0 | 0 | — |
case-18 | pass→pass | 18,523 | 16,817 | -9% | 1 | 1 | 0% | 2,741 | 7,005 | +156% | 0 | 0 | — |
case-19 | pass→pass | 13,725 | 19,445 | +42% | 1 | 1 | 0% | 2,261 | 6,395 | +183% | 0 | 0 | — |
case-20 | pass→pass | 9,075 | 11,329 | +25% | 1 | 1 | 0% | 1,225 | 6,025 | +392% | 0 | 0 | — |
case-21 | pass→pass | 21,371 | 22,915 | +7% | 1 | 1 | 0% | 3,904 | 7,482 | +92% | 0 | 0 | — |
case-22 | pass→pass | 20,357 | 19,805 | -3% | 1 | 1 | 0% | 3,695 | 7,794 | +111% | 0 | 0 | — |
case-23 | pass→pass | 24,551 | 36,816 | +50% | 1 | 1 | 0% | 4,414 | 8,625 | +95% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.