
Web Performance Optimization
- 397 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Use web-performance-optimization for development tasks
About
web-performance-optimization: A skill for development. This provides functionality for development workflows.
- web-performance-optimization
Web Performance Optimization by the numbers
- 397 all-time installs (skills.sh)
- +17 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,049 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill web-performance-optimizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 397 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Use web-performance-optimization for development tasks
Files
Web Performance Optimization
Overview
Implement performance optimization strategies including lazy loading, code splitting, caching, compression, and monitoring to improve Core Web Vitals and user experience.
When to Use
- Slow page load times
- High Largest Contentful Paint (LCP)
- Large bundle sizes
- Frequent Cumulative Layout Shift (CLS)
- Mobile performance issues
Code Splitting (React)
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}Webpack Bundle Optimization
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};Image Optimization
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img
src="image.jpg"
srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
loading="lazy"
decoding="async"
alt="Description"
>
</picture>Service Worker Caching
// sw.js
const CACHE_NAME = 'app-v1';
const ASSETS = ['/', '/index.html', '/main.js', '/styles.css'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});Core Web Vitals Monitoring
// Track LCP, CLS, INP (Note: INP replaced FID as of March 2024)
// sendToAnalytics is a placeholder function that sends metrics to your analytics endpoint
// Expected signature: sendToAnalytics({ metric: string, value: number }) => void
// Example implementation:
function sendToAnalytics({ metric, value }) {
// Replace with your analytics implementation (e.g., Google Analytics, Segment)
fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ metric, value, timestamp: Date.now() })
});
}
// Largest Contentful Paint (LCP)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`LCP: ${entry.startTime}ms`);
sendToAnalytics({ metric: 'LCP', value: entry.startTime });
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
// Cumulative Layout Shift (CLS)
new PerformanceObserver((list) => {
let cls = 0;
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) cls += entry.value;
}
sendToAnalytics({ metric: 'CLS', value: cls });
}).observe({ type: 'layout-shift', buffered: true });
// Interaction to Next Paint (INP) - replaces FID
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// INP measures responsiveness - duration of slowest interaction
const inp = entry.processingEnd - entry.processingStart;
console.log(`INP: ${inp}ms`);
sendToAnalytics({ metric: 'INP', value: inp });
}
}).observe({ type: 'event', buffered: true }); // 'event' captures interaction eventsPerformance Targets
| Metric | Good | Needs Improvement |
|---|---|---|
| LCP | <2.5s | 2.5-4s |
| INP | <200ms | 200-500ms |
| CLS | <0.1 | 0.1-0.25 |
| TTI | <3.8s | 3.8-7.3s |
Note: INP (Interaction to Next Paint) replaced FID (First Input Delay) as a Core Web Vital in March 2024. INP provides a more comprehensive measure of page responsiveness by capturing the full duration of interactions, not just the input delay.
Compression (Nginx)
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
gzip_comp_level 6;Best Practices
- Minimize bundle size with code splitting
- Optimize images with appropriate formats
- Implement lazy loading strategically
- Use HTTP caching headers
- Enable gzip/brotli compression
- Monitor Core Web Vitals continuously
- Implement service workers
- Defer non-critical JavaScript
- Optimize critical rendering path
- Test on real devices and networks
Optimization Checklist
- [ ] Enable code splitting for routes
- [ ] Lazy load below-fold components
- [ ] Optimize and compress images
- [ ] Implement service worker caching
- [ ] Enable gzip/brotli compression
- [ ] Monitor Core Web Vitals
- [ ] Minimize render-blocking resources
Additional Configuration
See references/compression-monitoring.md for:
- Webpack compression plugin setup
- Apache .htaccess compression config
- TTFB monitoring implementation
- Puppeteer automation for measurement
See references/typescript-advanced.md for:
- TypeScript lazyLoad utility
- TypeScript image component
- Advanced service worker with offline fallback
- TerserPlugin configuration
- Complete PerformanceMetrics interface
Tools
- Lighthouse / PageSpeed Insights
- WebPageTest
- Chrome DevTools Performance tab
- web-vitals npm package
Resources
Compression & Monitoring
Webpack Compression Plugin
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192, // Only compress files > 8KB
minRatio: 0.8
})
]
};Apache .htaccess Compression
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/json
</IfModule>
# Cache static assets
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>TTFB Monitoring
// Track Time to First Byte
new PerformanceObserver((list) => {
const entries = list.getEntriesByType('navigation');
entries.forEach((entry) => {
const ttfb = entry.responseStart - entry.requestStart;
sendToAnalytics({ metric: 'TTFB', value: ttfb });
});
}).observe({ type: 'navigation', buffered: true });
// Complete Web Vitals tracking
function trackWebVitals() {
// TTFB
const navEntry = performance.getEntriesByType('navigation')[0];
const ttfb = navEntry.responseStart - navEntry.requestStart;
// FID (First Input Delay)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const fid = entry.processingStart - entry.startTime;
sendToAnalytics({ metric: 'FID', value: fid });
}
}).observe({ type: 'first-input', buffered: true });
sendToAnalytics({ metric: 'TTFB', value: ttfb });
}Puppeteer Performance Automation
const puppeteer = require('puppeteer');
async function measurePerformance(url) {
let browser;
try {
browser = await puppeteer.launch();
const page = await browser.newPage();
// Enable performance tracking
await page.setCacheEnabled(false);
const client = await page.target().createCDPSession();
await client.send('Performance.enable');
await page.goto(url, { waitUntil: 'networkidle0' });
// Get performance metrics
const metrics = await page.metrics();
// Access performance.timing directly without JSON round-trip
const performanceTiming = await page.evaluate(() => performance.timing);
// Calculate key metrics
const results = {
ttfb: performanceTiming.responseStart - performanceTiming.requestStart,
domContentLoaded: performanceTiming.domContentLoadedEventEnd - performanceTiming.navigationStart,
load: performanceTiming.loadEventEnd - performanceTiming.navigationStart,
jsHeapSize: metrics.JSHeapUsedSize / 1024 / 1024, // MB
};
// Get LCP - wait for page to become hidden to capture final value
// This prevents race condition where LCP updates after initial measurement
const lcp = await page.evaluate(() => {
return new Promise((resolve) => {
let latestLCP = 0;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
// Keep track of the latest LCP value
latestLCP = entries[entries.length - 1].startTime;
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
// Wait for page visibility to change to 'hidden' to get final LCP
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
observer.disconnect();
resolve(latestLCP);
}
}, { once: true });
// Fallback: resolve after a reasonable timeout if visibility never changes
setTimeout(() => {
observer.disconnect();
resolve(latestLCP);
}, 5000);
});
});
results.lcp = lcp;
return results;
} catch (error) {
console.error('Performance measurement failed:', error);
throw error;
} finally {
// Ensure browser is always closed, even if measurement fails
if (browser) {
await browser.close();
}
}
}
// Usage
measurePerformance('https://example.com').then(console.log);Analytics Integration
// Simple version - acceptable for optional telemetry
function sendToAnalytics({ metric, value }) {
fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metric,
value,
url: window.location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent
})
}).catch(console.error); // Silently fail - acceptable for non-critical telemetry
}
// Production version - with retries and fallback
function sendToAnalyticsWithRetry({ metric, value }, retries = 2) {
const payload = {
metric,
value,
url: window.location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent
};
const sendRequest = async (endpoint, attempt = 0) => {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
// Set reasonable timeout via signal
signal: AbortSignal.timeout(3000)
});
if (!response.ok && attempt < retries) {
// Retry on 5xx errors with exponential backoff
if (response.status >= 500) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
return sendRequest(endpoint, attempt + 1);
}
}
return response;
} catch (error) {
console.error(`Analytics send failed (attempt ${attempt + 1}):`, error);
// Retry on network errors
if (attempt < retries) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return sendRequest(endpoint, attempt + 1);
}
// Log to fallback endpoint after all retries exhausted
if (attempt >= retries) {
try {
await fetch('/api/analytics/fallback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...payload, error: error.message })
});
} catch (fallbackError) {
console.error('Fallback analytics also failed:', fallbackError);
// Optional: Store in localStorage for batch send later
storeForRetry(payload);
}
}
throw error;
}
};
// Fire and forget - don't block page
sendRequest('/api/analytics').catch(() => {
// Already logged and handled above
});
}
// Optional: Store failed metrics in localStorage for batch retry
function storeForRetry(payload) {
try {
const stored = JSON.parse(localStorage.getItem('pendingAnalytics') || '[]');
stored.push(payload);
// Limit to prevent unbounded growth
if (stored.length > 100) stored.shift();
localStorage.setItem('pendingAnalytics', JSON.stringify(stored));
} catch (e) {
console.error('Failed to store analytics locally:', e);
}
}
// Optional: Retry stored metrics on next page load
function retryStoredAnalytics() {
try {
const stored = JSON.parse(localStorage.getItem('pendingAnalytics') || '[]');
if (stored.length === 0) return;
fetch('/api/analytics/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(stored)
}).then(() => {
localStorage.removeItem('pendingAnalytics');
}).catch(console.error);
} catch (e) {
console.error('Failed to retry stored analytics:', e);
}
}
// Call on page load
if (typeof window !== 'undefined') {
window.addEventListener('load', retryStoredAnalytics);
}TypeScript Performance Optimizations
Lazy Load Utility
// utils/lazyLoad.ts
import React from 'react';
export const lazyLoad = (importStatement: Promise<any>) => {
return React.lazy(() =>
importStatement.then(module => ({
default: module.default
}))
);
};
// routes.tsx
import { lazyLoad } from './utils/lazyLoad';
export const routes = [
{
path: '/',
component: () => import('./pages/Home'),
lazy: lazyLoad(import('./pages/Home'))
},
{
path: '/dashboard',
lazy: lazyLoad(import('./pages/Dashboard'))
},
{
path: '/users',
lazy: lazyLoad(import('./pages/Users'))
}
];
// App.tsx with Suspense
import { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
export const App = () => {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
{routes.map(route => {
// JSX requires PascalCase for component references
const LazyComponent = route.lazy;
return (
<Route key={route.path} path={route.path} element={<LazyComponent />} />
);
})}
</Routes>
</Suspense>
</BrowserRouter>
);
};TypeScript Image Component
interface ImageProps {
src: string;
alt: string;
width: number;
height: number;
sizes?: string;
loading?: 'lazy' | 'eager';
}
const OptimizedImage: React.FC<ImageProps> = ({
src,
alt,
width,
height,
sizes = '100vw',
loading = 'lazy'
}) => {
const webpSrc = src.replace(/\.(jpg|png)$/, '.webp');
return (
<picture>
<source srcSet={webpSrc} type="image/webp" />
<img
src={src}
alt={alt}
width={width}
height={height}
sizes={sizes}
loading={loading}
decoding="async"
/>
</picture>
);
};Advanced Service Worker
// service-worker.ts
const CACHE_NAME = 'v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/css/style.css',
'/js/app.js'
];
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(ASSETS_TO_CACHE);
})
);
});
self.addEventListener('fetch', (event: FetchEvent) => {
// Cache first, fall back to network
event.respondWith(
caches.match(event.request).then(response => {
if (response) return response;
return fetch(event.request).then(response => {
// Clone the response
const cloned = response.clone();
// Cache successful responses
if (response.status === 200) {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, cloned);
});
}
return response;
}).catch(() => {
// Return offline page if available
return caches.match('/offline.html');
});
})
);
});
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.catch(err => console.error('SW registration failed:', err));
});
}TerserPlugin Configuration
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true
}
}
})
]
}
};Complete Performance Metrics Interface
// utils/performanceMonitor.ts
interface PerformanceMetrics {
fcp: number; // First Contentful Paint
lcp: number; // Largest Contentful Paint
cls: number; // Cumulative Layout Shift
fid: number; // First Input Delay
ttfb: number; // Time to First Byte
}
export const observeWebVitals = (callback: (metrics: Partial<PerformanceMetrics>) => void) => {
const metrics: Partial<PerformanceMetrics> = {};
// LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
callback(metrics);
});
try {
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
} catch (e) {
console.warn('LCP observer not supported');
}
// CLS
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as any).hadRecentInput) {
metrics.cls = (metrics.cls || 0) + (entry as any).value;
callback(metrics);
}
}
});
try {
clsObserver.observe({ entryTypes: ['layout-shift'] });
} catch (e) {
console.warn('CLS observer not supported');
}
// FID via INP (First Input Delay)
const inputObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const firstEntry = entries[0];
metrics.fid = firstEntry.processingDuration;
callback(metrics);
});
try {
// NOTE: 'event' is not a valid entryType - use only 'first-input'
// For INP tracking, use 'layout-shift', 'largest-contentful-paint', etc.
inputObserver.observe({ entryTypes: ['first-input'] });
} catch (e) {
console.warn('FID observer not supported');
}
// TTFB (Time to First Byte) - measure from fetch start to first byte
const navigationTiming = performance.getEntriesByType('navigation')[0];
if (navigationTiming) {
const nav = navigationTiming as PerformanceNavigationTiming;
// CORRECT: responseStart - fetchStart measures time from fetch start to first byte
// INCORRECT: responseStart - requestStart only measures request duration
metrics.ttfb = nav.responseStart - nav.fetchStart;
callback(metrics);
}
};
// Usage
observeWebVitals((metrics) => {
console.log('Performance metrics:', metrics);
// Send to analytics
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(metrics)
});
});
// Chrome DevTools Protocol for performance testing
import puppeteer from 'puppeteer';
async function measurePagePerformance(url: string) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const metrics = JSON.parse(
await page.evaluate(() => JSON.stringify(window.performance))
);
console.log('Page Load Time:', metrics.timing.loadEventEnd - metrics.timing.navigationStart);
console.log('DOM Content Loaded:', metrics.timing.domContentLoadedEventEnd - metrics.timing.navigationStart);
await browser.close();
}