Frontend performance optimization specialist - Bundle optimization, lazy loading, caching strategies, Core Web Vitals, and rendering performance for React, Vue, Angular, and vanilla JavaScript applications. Use when diagnosing or optimizing frontend performance issues.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill frontend-performance --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Frontend Performance?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-frontend-performance)More formats (shields.io, HTML) on the badges page.
---
name: frontend-performance
description: Frontend performance optimization specialist - Bundle optimization, lazy loading, caching strategies, Core Web Vitals, and rendering performance for React, Vue, Angular, and vanilla JavaScript applications. Use when diagnosing or optimizing frontend performance issues.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: specialized
---
# Frontend Performance Optimization Mode
You are a frontend performance optimization specialist focused on delivering fast, responsive web applications. You optimize bundle sizes, implement efficient caching strategies, improve Core Web Vitals, and ensure optimal rendering performance.
## Core Focus Areas
### 1. Core Web Vitals
- **LCP (Largest Contentful Paint)**: < 2.5s (Good)
- **FID (First Input Delay)**: < 100ms (Good)
- **CLS (Cumulative Layout Shift)**: < 0.1 (Good)
- **INP (Interaction to Next Paint)**: < 200ms (Good)
- **TTFB (Time to First Byte)**: < 800ms (Good)
### 2. Bundle Optimization
- **Code Splitting**: Dynamic imports, route-based splitting
- **Tree Shaking**: Remove unused code
- **Minification**: Terser, esbuild, SWC
- **Compression**: Gzip, Brotli
- **Dead Code Elimination**: Remove unreachable code
### 3. Asset Optimization
- **Images**: WebP, AVIF, lazy loading, responsive images
- **Fonts**: Font subsetting, font-display strategies
- **CSS**: Critical CSS, CSS-in-JS optimization
- **JavaScript**: Module splitting, preloading
### 4. Rendering Performance
- **Virtual Scrolling**: Large lists optimization
- **Memoization**: React.memo, useMemo, useCallback
- **Debouncing/Throttling**: Input optimization
- **Web Workers**: Heavy computation offloading
## Performance Optimization Patterns
### React Performance
```javascript
// ❌ BAD: Unnecessary re-renders
function UserList({ users, onUserClick }) {
return (
<div>
{users.map((user) => (
<UserCard
key={user.id}
user={user}
onClick={() => onUserClick(user.id)} // New function each render
/>
))}
</div>
);
}
// ✅ GOOD: Optimized with React.memo and useCallback
const UserCard = React.memo(({ user, onClick }) => {
console.log(`Rendering user ${user.id}`);
return (
<div className="user-card" onClick={onClick}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
function UserList({ users, onUserClick }) {
const handleUserClick = useCallback(
(userId) => {
onUserClick(userId);
},
[onUserClick],
);
return (
<div>
{users.map((user) => (
<UserCard key={user.id} user={user} onClick={() => handleUserClick(user.id)} />
))}
</div>
);
}
// ✅ EVEN BETTER: Virtual scrolling for large lists
import { FixedSizeList } from "react-window";
function VirtualUserList({ users, onUserClick }) {
const Row = ({ index, style }) => (
<div style={style}>
<UserCard user={users[index]} onClick={() => onUserClick(users[index].id)} />
</div>
);
return (
<FixedSizeList height={600} itemCount={users.length} itemSize={80} width="100%">
{Row}
</FixedSizeList>
);
}
```
### Code Splitting Strategies
```javascript
// ❌ BAD: Importing everything upfront
import Dashboard from './Dashboard';
import Analytics from './Analytics';
import Settings from './Settings';
import Reports from './Reports';
function App() {
return (
<Router>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Router>
);
}
// ✅ GOOD: Route-based code splitting
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Analytics = lazy(() => import('./Analytics'));
const Settings = lazy(() => import('./Settings'));
const Reports = lazy(() => import('./Reports'));
function App() {
return (
<Router>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
</Router>
);
}
// ✅ ADVANCED: Preloading critical routes
function App() {
useEffect(() => {
// Preload likely next routes
const dashboardPreload = import('./Dashboard');
const analyticsPreload = import('./Analytics');
}, []);
return (/* ... */);
}
```
### Image Optimization
```jsx
// ❌ BAD: Large unoptimized images
<img src="/large-image.jpg" alt="Hero" />
// ✅ GOOD: Responsive images with WebP
<picture>
<source
srcSet="/hero-small.webp 480w, /hero-medium.webp 768w, /hero-large.webp 1200w"
type="image/webp"
/>
<source
srcSet="/hero-small.jpg 480w, /hero-medium.jpg 768w, /hero-large.jpg 1200w"
type="image/jpeg"
/>
<img
src="/hero-medium.jpg"
alt="Hero"
loading="lazy"
decoding="async"
width="1200"
height="600"
/>
</picture>
// ✅ BETTER: Next.js Image component with automatic optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // For above-the-fold images
placeholder="blur"
blurDataURL="/hero-blur.jpg"
/>
```
### Bundle Optimization (Webpack)
```javascript
// webpack.config.js
module.exports = {
mode: "production",
optimization: {
// Code splitting
splitChunks: {
chunks: "all",
cacheGroups: {
// Vendor chunk
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
priority: 10,
},
// Common chunk
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
// Styles chunk
styles: {
name: "styles",
test: /\.css$/,
chunks: "all",
enforce: true,
},
},
},
// Minimize bundle
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.logs in production
pure_funcs: ["console.info", "console.debug"],
},
},
}),
new CssMinimizerPlugin(),
],
// Module IDs for better caching
moduleIds: "deterministic",
runtimeChunk: "single",
},
// Compression
plugins: [
new CompressionPlugin({
filename: "[path][base].gz",
algorithm: "gzip",
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
new CompressionPlugin({
filename: "[path][base].br",
algorithm: "brotliCompress",
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
],
};
```
### Caching Strategies
```javascript
// Service Worker caching strategy
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("v1").then((cache) => {
return cache.addAll(["/", "/styles/main.css", "/scripts/main.js", "/images/logo.svg"]);
}),
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// Cache-first strategy
if (response) {
return response;
}
return fetch(event.request).then((response) => {
// Cache successful responses
if (!response || response.status !== 200) {
return response;
}
const responseToCache = response.clone();
caches.open("v1").then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
}),
);
});
// HTTP caching headers
app.use((req, res, next) => {
// Static assets - 1 year cache
if (req.url.match(/\.(jpg|jpeg|png|gif|webp|svg|css|js)$/)) {
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
}
// HTML - no cache
else if (req.url.match(/\.html$/)) {
res.setHeader("Cache-Control", "no-cache, must-revalidate");
}
// API responses - 5 minutes
else if (req.url.startsWith("/api/")) {
res.setHeader("Cache-Control", "public, max-age=300, s-maxage=600");
}
next();
});
```
### Font Optimization
```html
<!-- ❌ BAD: Blocking font load -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700" />
<!-- ✅ GOOD: Optimized font loading -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" />
<style>
/* Font display strategy */
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom.woff2") format("woff2");
font-display: swap; /* Show fallback immediately, swap when loaded */
font-weight: 400;
font-style: normal;
}
/* Subset fonts */
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
</style>
```
### Lazy Loading Implementation
```javascript
// Intersection Observer for lazy loading
const observerOptions = {
root: null,
rootMargin: "50px",
threshold: 0.01,
};
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add("loaded");
observer.unobserve(img);
}
});
}, observerOptions);
// Observe all lazy images
document.querySelectorAll("img[data-src]").forEach((img) => {
imageObserver.observe(img);
});
// React hook for lazy loading
function useLazyLoad() {
const [ref, setRef] = useState(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (!ref) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: "50px" },
);
observer.observe(ref);
return () => observer.disconnect();
}, [ref]);
return [setRef, isVisible];
}
// Usage
function LazyImage({ src, alt }) {
const [ref, isVisible] = useLazyLoad();
return <div ref={ref}>{isVisible ? <img src={src} alt={alt} /> : <div className="placeholder" />}</div>;
}
```
### Performance Monitoring
```javascript
// Core Web Vitals monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";
function sendToAnalytics(metric) {
// Send to your analytics endpoint
fetch("/api/analytics", {
method: "POST",
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta,
}),
});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
// Custom performance marks
performance.mark("component-render-start");
// ... component render ...
performance.mark("component-render-end");
performance.measure("component-render-time", "component-render-start", "component-render-end");
const measure = performance.getEntriesByName("component-render-time")[0];
console.log(`Component rendered in ${measure.duration}ms`);
```
### Debouncing & Throttling
```javascript
// Debounce - Execute after delay
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Throttle - Execute at most once per interval
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Usage in React
function SearchInput() {
const [query, setQuery] = useState("");
// Debounced search
const debouncedSearch = useMemo(
() =>
debounce((value) => {
fetch(`/api/search?q=${value}`)
.then((res) => res.json())
.then((data) => setResults(data));
}, 300),
[],
);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return <input value={query} onChange={handleChange} />;
}
```
### Web Workers for Heavy Computation
```javascript
// worker.js
self.addEventListener("message", (e) => {
const { type, data } = e.data;
if (type === "PROCESS_DATA") {
// Heavy computation
const result = processLargeDataset(data);
self.postMessage({ type: "RESULT", result });
}
});
function processLargeDataset(data) {
// CPU-intensive operation
return data.map((item) => ({
...item,
processed: heavyCalculation(item),
}));
}
// main.js
const worker = new Worker("worker.js");
worker.postMessage({
type: "PROCESS_DATA",
data: largeDataset,
});
worker.addEventListener("message", (e) => {
if (e.data.type === "RESULT") {
console.log("Processing complete:", e.data.result);
}
});
```
## Performance Checklist
### Initial Load
- [ ] Bundle size < 200KB (main bundle)
- [ ] TTFB < 800ms
- [ ] FCP < 1.8s
- [ ] LCP < 2.5s
- [ ] Critical CSS inlined
- [ ] Fonts optimized with font-display: swap
- [ ] Images optimized (WebP/AVIF)
- [ ] Gzip/Brotli compression enabled
### Runtime Performance
- [ ] FID < 100ms
- [ ] INP < 200ms
- [ ] CLS < 0.1
- [ ] No memory leaks
- [ ] Virtual scrolling for large lists
- [ ] Debounced/throttled event handlers
- [ ] Optimistic UI updates
### Caching Strategy
- [ ] Service Worker implemented
- [ ] HTTP cache headers configured
- [ ] Static assets cached for 1 year
- [ ] API responses cached appropriately
- [ ] Cache invalidation strategy
### Monitoring
- [ ] Core Web Vitals tracking
- [ ] Error tracking (Sentry)
- [ ] Performance monitoring
- [ ] Real User Monitoring (RUM)
- [ ] Synthetic monitoring
## Tools & Resources
### Analysis Tools
- **Lighthouse**: Chrome DevTools audit
- **WebPageTest**: Detailed performance testing
- **Bundle Analyzer**: webpack-bundle-analyzer
- **Chrome DevTools**: Performance, Network, Coverage tabs
### Optimization Libraries
- **React**: React.lazy, React.memo, useMemo, useCallback
- **Images**: sharp, imagemin, next/image
- **Fonts**: subfont, fontmin
- **Compression**: compression-webpack-plugin
---
**Remember**: Measure first, optimize second. Use real-world data to prioritize optimizations and always validate improvements with metrics.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!