| name | frontend-performance |
| description | Frontend performance optimization for Angular and React/Next.js. Use when asked to "optimize performance", "improve Core Web Vitals", "reduce bundle size", "fix re-renders", "lazy loading", "lighthouse score", or "speed up the app". |
| license | MIT |
| metadata | {"author":"pragma-frontend-performance","version":"1.0","reference":"web.dev/performance, Core Web Vitals"} |
Frontend Performance — Complete Knowledge Base
Performance optimization patterns for modern frontend applications. Covers Core Web Vitals, rendering optimization, bundle analysis, lazy loading, caching, images, fonts, and framework-specific patterns.
P1 — Core Web Vitals
| Metric | What It Measures | Good | Needs Improvement | Poor |
|---|
| LCP (Largest Contentful Paint) | Loading performance | ≤2.5s | 2.5s–4.0s | >4.0s |
| INP (Interaction to Next Paint) | Responsiveness | ≤200ms | 200ms–500ms | >500ms |
| CLS (Cumulative Layout Shift) | Visual stability | ≤0.1 | 0.1–0.25 | >0.25 |
LCP Optimization
<img src={heroImage} />
const HeroSection = lazy(() => import('./Hero'));
<link rel="preload" as="image" href="/hero.webp" fetchPriority="high" />
<Image src="/hero.webp" priority alt="Hero" width={1200} height={600} />
<img ngSrc="/hero.webp" priority width="1200" height="600" alt="Hero" />
INP Optimization
function handleClick() {
const result = heavyComputation(data);
setResult(result);
}
function handleClick() {
requestIdleCallback(() => {
const result = heavyComputation(data);
setResult(result);
});
}
const worker = new Worker('/workers/heavy-task.js');
worker.postMessage(data);
worker.onmessage = (e) => setResult(e.data);
import { startTransition } from 'react';
function handleInput(value: string) {
setInputValue(value);
startTransition(() => {
setFilteredResults(filter(value));
});
}
import { useDebouncedCallback } from 'use-debounce';
const handleSearch = useDebouncedCallback((term: string) => {
fetchResults(term);
}, 300);
CLS Optimization
<img src={url} />
{isLoaded && <div>Content</div>}
<div style={{ height: dynamicHeight }} />
<img src={url} width={400} height={300} alt="Product" />
<video width={640} height={360} />
<iframe width={560} height={315} />
<div className="aspect-video">
<img src={url} className="object-cover w-full h-full" alt="" />
</div>
{isLoading ? <ProductCardSkeleton /> : <ProductCard data={product} />}
.sidebar { contain: layout style; }
P2 — Bundle Size Optimization
Analysis
npx @next/bundle-analyzer
npx webpack-bundle-analyzer
npx vite-bundle-visualizer
npx source-map-explorer dist/**/*.js
npx import-cost
Common Bloaters
| Package | Bloat | Fix |
|---|
lodash (full) | ~70KB | Use lodash-es or individual imports |
moment | ~290KB | Replace with date-fns (~7KB per function) or dayjs (~2KB) |
axios | ~13KB | Use native fetch (0KB) |
Barrel files (index.ts) | Pulls entire module | Import specific file paths |
| Icon libraries (full) | ~500KB+ | Import individual icons only |
@mui/material (full) | ~300KB | Use path imports: @mui/material/Button |
Tree Shaking
import _ from 'lodash';
import * as utils from './utils';
import { Button } from '@mui/material';
import debounce from 'lodash-es/debounce';
import { formatDate } from './utils/date';
import Button from '@mui/material/Button';
{
"sideEffects": false
"sideEffects": ["*.css", "./src/polyfills.ts"]
}
Code Splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <ChartSkeleton />,
ssr: false,
});
const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
},
];
@defer (on viewport) {
<heavy-chart [data]="chartData" />
} @placeholder {
<chart-skeleton />
}
P3 — Rendering Performance
React — Preventing Unnecessary Re-renders
function Parent() {
return <Child style={{ color: 'red' }} onClick={() => doSomething()} />;
}
function Parent() {
const style = useMemo(() => ({ color: 'red' }), []);
const handleClick = useCallback(() => doSomething(), []);
return <Child style={style} onClick={handleClick} />;
}
const ExpensiveChild = memo(function ExpensiveChild({ data }: Props) {
return <div>{/* complex rendering */}</div>;
});
{items.map((item) => (
<ListItem key={item.id} item={item} />
))}
function Page() {
const [search, setSearch] = useState('');
return (
<>
<SearchBar value={search} onChange={setSearch} />
<ExpensiveList /> {/* Re-renders on every keystroke! */}
<ExpensiveChart /> {/* Re-renders on every keystroke! */}
</>
);
}
function Page() {
return (
<>
<SearchSection /> {/* State lives here, isolated */}
<ExpensiveList /> {/* Not affected */}
<ExpensiveChart /> {/* Not affected */}
</>
);
}
Angular — Change Detection
@Component({
selector: 'app-product-card',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
template: `<div>{{ product().name }}</div>`,
})
export class ProductCardComponent {
product = input.required<Product>();
}
readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);
@for (item of items(); track item.id) {
<app-item [item]="item" />
}
@defer (on viewport) {
<app-heavy-section />
} @placeholder {
<section-skeleton />
}
P4 — Image & Font Optimization
Images
import Image from 'next/image';
<Image
src="/product.jpg"
alt="Product"
width={400}
height={300}
placeholder="blur"
blurDataURL={blurHash}
sizes="(max-width: 768px) 100vw, 400px"
/>
<img ngSrc="/product.jpg" width="400" height="300" alt="Product"
placeholder loading="lazy" />
<picture>
<source srcset="/img.avif" type="image/avif" />
<source srcset="/img.webp" type="image/webp" />
<img src="/img.jpg" alt="Fallback" width="400" height="300" loading="lazy" />
</picture>
<img src="/product.jpg" loading="lazy" alt="" />
<img src="/hero.jpg" fetchPriority="high" alt="" />
Fonts
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
<link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossOrigin="" />
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter.woff2') format('woff2');
font-display: swap;
}
P5 — Caching & Data Fetching
const { data } = useQuery({
queryKey: ['products', filters],
queryFn: () => fetchProducts(filters),
staleTime: 5 * 60 * 1000,
gcTime: 30 * 60 * 1000,
});
const data = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 },
});
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
const [user, products, notifications] = await Promise.all([
fetchUser(id),
fetchProducts(),
fetchNotifications(),
]);
readonly productResource = resource({
request: () => this.productId(),
loader: ({ request: id }) => fetchProduct(id),
});
P6 — Third-Party Script Impact
import Script from 'next/script';
<Script src="https://analytics.example.com/script.js" strategy="lazyOnload" />
import { Partytown } from '@builder.io/partytown/react';
<Partytown forward={['dataLayer.push']} />
P7 — Performance Monitoring
Lighthouse CI
npm install -g @lhci/cli
lhci autorun --config=lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"first-contentful-paint": ["error", { "maxNumericValue": 1500 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"interactive": ["error", { "maxNumericValue": 3500 }]
}
}
}
}
Real User Monitoring (RUM)
import { onLCP, onINP, onCLS } from 'web-vitals';
function reportMetric(metric: { name: string; value: number; id: string }) {
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(metric),
keepalive: true,
});
}
onLCP(reportMetric);
onINP(reportMetric);
onCLS(reportMetric);
export function reportWebVitals(metric: NextWebVitalsMetric) {
}
Quick Reference — Performance Checklist
| Category | Check | Impact |
|---|
| CWV-LCP | LCP image preloaded with priority/fetchPriority="high" | CRITICAL |
| CWV-LCP | No lazy loading on above-the-fold content | HIGH |
| CWV-LCP | SSR/SSG for main content (not client-side fetch + spinner) | HIGH |
| CWV-INP | No long tasks (>50ms) on main thread | CRITICAL |
| CWV-INP | Heavy computation in Web Workers | HIGH |
| CWV-INP | Debounced input handlers | MEDIUM |
| CWV-CLS | All images/video have explicit width + height | CRITICAL |
| CWV-CLS | Skeleton UI reserves space for dynamic content | HIGH |
| Bundle | No full library imports (lodash, moment, MUI barrel) | HIGH |
| Bundle | Routes lazy-loaded / code-split | HIGH |
| Bundle | Tree shaking working (sideEffects: false) | MEDIUM |
| Render | React.memo on expensive children | HIGH |
| Render | Angular OnPush + signals | HIGH |
| Render | Stable keys on lists (not index) | MEDIUM |
| Render | State kept close to where it's used | MEDIUM |
| Images | WebP/AVIF formats, lazy loaded, responsive sizes | HIGH |
| Fonts | Self-hosted, preloaded, font-display: swap | MEDIUM |
| Data | Parallel fetches (no waterfalls) | HIGH |
| Data | Proper cache strategy (staleTime, revalidate) | MEDIUM |
| 3rd Party | Analytics/ads loaded with lazyOnload or web worker | HIGH |
| Monitor | Lighthouse CI with score thresholds | HIGH |
| Monitor | Real User Monitoring (web-vitals) | MEDIUM |