Install
openclaw skills install afrexai-web-performance-enginePerforms comprehensive web performance audits, diagnoses bottlenecks, and provides targeted fixes for server, rendering, hero element, JavaScript, and layout...
openclaw skills install afrexai-web-performance-engineComplete web performance optimization system. Audit, diagnose, fix, and monitor — no external tools required (but integrates with Lighthouse, WebPageTest, Chrome DevTools when available).
Run these checks in order. Stop when you find the bottleneck tier.
Tier 1 — Critical (blocks rendering):
Tier 2 — Important (affects experience):
Tier 3 — Polish (competitive edge):
audit:
url: ""
device: "mobile" # mobile | desktop | both
connection: "4G" # 3G | 4G | fiber
region: "" # closest to target users
scores:
performance: null # 0-100
fcp_ms: null
lcp_ms: null
tbt_ms: null
cls: null
inp_ms: null
ttfb_ms: null
page_weight:
total_kb: null
html_kb: null
css_kb: null
js_kb: null
images_kb: null
fonts_kb: null
other_kb: null
requests:
total: null
by_type: {}
third_party_count: null
third_party_kb: null
If no Lighthouse/DevTools available, use web-based tools:
web_fetch "https://pagespeed.web.dev/analysis?url={encoded_url}" — Google's free toolweb_search "webpagetest {url}" — find cached resultsweb_search "site:{domain} core web vitals" — find CrUX data<head> for obvious issues: render-blocking CSS/JS, missing preloads, no meta viewportDNS → TCP → TLS → TTFB → HTML Parse → CSSOM → Render Tree → FCP → LCP
↓
JS Download → Parse → Execute → INP
Bottleneck Decision Tree:
High TTFB (>800ms)?
├─ YES → Phase 3A: Server optimization
└─ NO → High FCP (>1.8s)?
├─ YES → Phase 3B: Render-blocking resources
└─ NO → High LCP (>2.5s)?
├─ YES → Phase 3C: Hero element optimization
└─ NO → High TBT (>200ms)?
├─ YES → Phase 3D: JavaScript optimization
└─ NO → High CLS (>0.1)?
├─ YES → Phase 3E: Layout stability
└─ NO → High INP (>200ms)?
├─ YES → Phase 3F: Interaction optimization
└─ NO → ✅ Performance is good!
Rate each resource by impact:
| Factor | Weight | Score 1 | Score 3 | Score 5 |
|---|---|---|---|---|
| Size (KB) | 3x | <10 | 10-100 | >100 |
| Render-blocking | 5x | No | Partial | Full |
| Above-fold impact | 4x | None | Indirect | Direct |
| Cacheable | 2x | Long cache | Short cache | No cache |
| Compressible | 2x | Already done | Possible | Not compressed |
Priority = Sum(Factor × Weight). Fix highest scores first.
Quick wins:
# CDN: If no CDN, this is #1 priority
# Check: curl -sI {url} | grep -i 'x-cache\|cf-cache\|x-cdn'
# Compression: Must have brotli or gzip
# Check: curl -sI -H "Accept-Encoding: br,gzip" {url} | grep -i content-encoding
# HTTP/2 or HTTP/3
# Check: curl -sI --http2 {url} | head -1
Server-side checklist:
Cache headers template:
# Static assets (CSS, JS, images, fonts)
Cache-Control: public, max-age=31536000, immutable
# HTML pages
Cache-Control: public, max-age=0, must-revalidate
# API responses
Cache-Control: private, max-age=60, stale-while-revalidate=300
CSS optimization:
<!-- BEFORE: Render-blocking -->
<link rel="stylesheet" href="styles.css">
<!-- AFTER: Critical CSS inline + async load -->
<style>/* Critical above-fold CSS here (< 14KB) */</style>
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
Rules:
@import (creates sequential loading)JavaScript optimization:
<!-- BEFORE: Render-blocking -->
<script src="app.js"></script>
<!-- AFTER: Non-blocking -->
<script src="app.js" defer></script>
<!-- OR: Independent scripts -->
<script src="analytics.js" async></script>
Rules:
defer for app scripts (maintains order, runs after parse)async for independent scripts (analytics, ads)<script> in <head> without defer/asyncLCP element types and fixes:
| LCP Element | Fix |
|---|---|
<img> | Preload + responsive + modern format |
<video> poster | Preload poster image |
CSS background-image | Preload + inline critical CSS |
| Text block | Preload font + font-display: optional |
Image optimization checklist:
<!-- Optimal hero image -->
<link rel="preload" as="image" href="hero.webp"
imagesrcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
imagesizes="100vw">
<img src="hero.webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
sizes="100vw"
width="1200" height="600"
alt="Hero description"
fetchpriority="high"
decoding="async">
Image format decision:
Photo/complex image? → WebP (25-35% smaller than JPEG)
→ AVIF (50% smaller, but slower encode)
Simple graphic/logo? → SVG (scalable, tiny)
→ PNG only if transparency needed
Animation? → WebM/MP4 video (not GIF — 90% smaller)
Image size targets:
| Viewport | Max width | Target KB |
|---|---|---|
| Mobile | 400px | < 50KB |
| Tablet | 800px | < 100KB |
| Desktop | 1200px | < 150KB |
| Hero/banner | 1600px | < 200KB |
Bundle analysis approach:
web_fetch the page, count <script> tagsCommon JS bloat and replacements:
| Library | Size | Alternative | Size |
|---|---|---|---|
| moment.js | 67KB | date-fns | 2-10KB |
| lodash (full) | 71KB | lodash-es (tree-shake) | 2-5KB |
| jQuery | 87KB | vanilla JS | 0KB |
| animate.css | 80KB | CSS animations | 1-2KB |
| chart.js | 60KB | lightweight-charts | 40KB |
Code splitting rules:
const Chart = lazy(() => import('./Chart'))Long task breaking:
// BEFORE: Blocks main thread 200ms+
function processLargeList(items) {
items.forEach(item => heavyComputation(item));
}
// AFTER: Yields to main thread
async function processLargeList(items) {
for (const item of items) {
heavyComputation(item);
// Yield every 50ms
if (performance.now() - start > 50) {
await scheduler.yield(); // or setTimeout(0)
start = performance.now();
}
}
}
Top CLS causes and fixes:
| Cause | Fix |
|---|---|
| Images without dimensions | Always set width + height |
| Ads/embeds without space | Reserve space with aspect-ratio or min-height |
| Dynamic content injection | Use CSS contain or reserved space |
| Web fonts causing reflow | font-display: optional or swap with size-adjust |
| Late-loading CSS | Inline critical CSS |
Anti-CLS patterns:
/* Reserve space for dynamic content */
.ad-slot { min-height: 250px; }
.embed-container { aspect-ratio: 16/9; }
/* Prevent font swap reflow */
@font-face {
font-family: 'Brand';
src: url('brand.woff2') format('woff2');
font-display: optional; /* No swap = no shift */
size-adjust: 105%; /* Match fallback metrics */
}
/* Contain layout shifts */
.dynamic-widget {
contain: layout;
min-height: 200px;
}
Event handler rules:
requestAnimationFrame for visual updatescontent-visibility: auto for off-screen contentInput responsiveness:
// BEFORE: Blocks during type
input.addEventListener('input', (e) => {
expensiveFilter(e.target.value); // 100ms+
});
// AFTER: Debounced + visual feedback
input.addEventListener('input', (e) => {
showSpinner(); // Instant visual feedback
debounce(() => expensiveFilter(e.target.value), 150);
});
<!-- Preconnect: Third-party origins you'll need soon -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- DNS-prefetch: Third-party origins you might need -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<!-- Preload: Critical resources for THIS page -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.webp" as="image">
<link rel="preload" href="brand.woff2" as="font" type="font/woff2" crossorigin>
<!-- Prefetch: Resources for NEXT page (low priority) -->
<link rel="prefetch" href="/next-page.js">
<!-- Modulepreload: ES modules -->
<link rel="modulepreload" href="app.mjs">
Rules:
Above fold (viewport): fetchpriority="high", no lazy
Below fold (1-2 screens): loading="lazy", decoding="async"
Way below fold: Intersection Observer, load on demand
Off-screen widgets: content-visibility: auto
/* Optimal font loading */
@font-face {
font-family: 'Brand';
src: url('brand.woff2') format('woff2');
font-display: swap;
unicode-range: U+0000-00FF; /* Latin only if applicable */
}
Font checklist:
System font stacks:
/* Modern system fonts — zero network cost */
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Monospace */
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
third_party_audit:
- script: "Google Analytics 4"
size_kb: 45
blocks_render: false
loads_more_scripts: true
total_impact_kb: 90
essential: true
mitigation: "gtag async, delay until interaction"
- script: "Intercom chat widget"
size_kb: 200
blocks_render: false
loads_more_scripts: true
total_impact_kb: 450
essential: false
mitigation: "Load on scroll/click, not page load"
Third-party loading strategies:
// Strategy 1: Load on interaction
document.addEventListener('scroll', () => {
loadThirdParty('chat-widget.js');
}, { once: true });
// Strategy 2: Load after page is idle
requestIdleCallback(() => {
loadThirdParty('analytics.js');
});
// Strategy 3: Facade pattern (show placeholder until needed)
chatButton.addEventListener('click', () => {
loadThirdParty('intercom.js').then(() => Intercom('show'));
});
Rules:
rel="noopener" on all external linksTargets (mobile on 4G):
| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| FCP | < 1.8s | 1.8-3.0s | > 3.0s |
| LCP | < 2.5s | 2.5-4.0s | > 4.0s |
| TBT | < 200ms | 200-600ms | > 600ms |
| CLS | < 0.1 | 0.1-0.25 | > 0.25 |
| INP | < 200ms | 200-500ms | > 500ms |
Mobile-specific checklist:
performance_budget:
metrics:
lcp_ms: 2500
fcp_ms: 1800
tbt_ms: 200
cls: 0.1
inp_ms: 200
resources:
total_kb: 1500
js_kb: 350
css_kb: 80
images_kb: 800
fonts_kb: 100
requests:
total: 60
third_party: 15
lighthouse:
performance: 90
accessibility: 90
best_practices: 90
seo: 90
Budget enforcement rules:
# Weekly performance check
date: "YYYY-MM-DD"
url: ""
device: "mobile"
scores:
lighthouse: null
lcp: null
fcp: null
tbt: null
cls: null
trend: "improving | stable | degrading"
regressions: []
actions: []
Rate the site 0-100:
| Dimension | Weight | 0-2 | 3-4 | 5 |
|---|---|---|---|---|
| Core Web Vitals | 25% | All red | Mixed | All green |
| Page weight | 15% | >5MB | 2-5MB | <2MB |
| Caching strategy | 15% | None | Partial | Full with immutable |
| Render path | 15% | Multiple blockers | Some optimized | Clean critical path |
| Image optimization | 10% | Unoptimized | Partially | WebP/AVIF + responsive |
| JavaScript health | 10% | >1MB, no splitting | Some splitting | <350KB, code-split |
| Third-party control | 5% | Unmanaged | Some deferred | All managed + budgeted |
| Mobile experience | 5% | Desktop-only | Responsive | Mobile-first optimized |
Score interpretation:
next/image (auto WebP, lazy, blur placeholder)dynamic() for heavy components@next/bundle-analyzer// Cache-first for static assets
self.addEventListener('fetch', (event) => {
if (event.request.url.match(/\.(css|js|woff2|webp|avif)$/)) {
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
);
}
});
// Predictive prefetch on hover
document.querySelectorAll('a').forEach(link => {
link.addEventListener('mouseenter', () => {
const prefetch = document.createElement('link');
prefetch.rel = 'prefetch';
prefetch.href = link.href;
document.head.appendChild(prefetch);
}, { once: true });
});
// Report Core Web Vitals
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Send to analytics
sendToAnalytics({
metric: entry.name,
value: entry.value,
rating: entry.rating, // "good" | "needs-improvement" | "poor"
});
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
Infinite scroll / pagination:
content-visibility: auto for off-screen itemsSPAs with client-side routing:
E-commerce product pages:
Media-heavy sites:
<video> not GIF (90% smaller)