# Frontend Performance Optimization: Full-Stack Enhancement from Loading to Rendering
Master Core Web Vitals, resource optimization, rendering performance, caching strategies and monitoring
Frontend performance directly impacts user experience and business conversion. This article explores core strategies and best practices for frontend performance optimization.
Core Web Vitals
Key Metrics
Core Web Vitals Metrics:
┌─────────────────────────────────────────────────────┐
│ │
│ LCP (Largest Contentful Paint) │
│ ├── Largest content paint time │
│ ├── Target: < 2.5s │
│ └── Affects: Images, fonts, large text blocks │
│ │
│ INP (Interaction to Next Paint) │
│ ├── Interaction to next paint │
│ ├── Target: < 200ms │
│ └── Affects: JS execution, event handling │
│ │
│ CLS (Cumulative Layout Shift) │
│ ├── Cumulative layout shift │
│ ├── Target: < 0.1 │
│ └── Affects: Image size, dynamic content, fonts │
│ │
└─────────────────────────────────────────────────────┘
Measurement Methods
// Using web-vitals library
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric: Metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
});
// Use sendBeacon to ensure data is sent
navigator.sendBeacon('/api/analytics', body);
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
// Performance Observer API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime}ms`);
}
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'layout-shift'] });
Resource Loading Optimization
Image Optimization
// Modern image formats and responsive images
function OptimizedImage({ src, alt }: { src: string; alt: string }) {
return (
<picture>
<source
srcSet={`${src}.avif 1x, ${src}@2x.avif 2x`}
type="image/avif"
/>
<source
srcSet={`${src}.webp 1x, ${src}@2x.webp 2x`}
type="image/webp"
/>
<img
src={`${src}.jpg`}
srcSet={`${src}.jpg 1x, ${src}@2x.jpg 2x`}
alt={alt}
loading="lazy"
decoding="async"
width={800}
height={600}
/>
</picture>
);
}
// Image preloading
function preloadCriticalImage(src: string) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = src;
link.fetchPriority = 'high';
document.head.appendChild(link);
}
Font Optimization
/* Font display strategy */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* Use system font until custom font loads */
unicode-range: U+0000-00FF; /* Only load needed characters */
}
/* Preload critical fonts */
<link
rel="preload"
href="/fonts/custom.woff2"
as="font"
type="font/woff2"
crossorigin
/>
/* Font subsetting - reduce font file size */
/* Use fonttools or glyphhanger tools */
Code Splitting
// React lazy loading
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Route-level code splitting
const routes = [
{
path: '/dashboard',
component: lazy(() => import('./pages/Dashboard')),
preload: () => import('./pages/Dashboard'),
},
];
// Prefetch next likely page
function prefetchRoute(path: string) {
const route = routes.find(r => r.path === path);
route?.preload();
}
Resource Preloading
<!-- DNS prefetch -->
<link rel="dns-prefetch" href="//api.example.com" />
<!-- Preconnect -->
<link rel="preconnect" href="https://api.example.com" crossorigin />
<!-- Preload critical resources -->
<link rel="preload" href="/critical.css" as="style" />
<link rel="preload" href="/main.js" as="script" />
<!-- Prefetch next page resources -->
<link rel="prefetch" href="/next-page.js" />
<!-- Module preload -->
<link rel="modulepreload" href="/modules/app.js" />
Rendering Performance
Virtual List
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
Avoid Layout Thrashing
// ❌ Triggers multiple reflows
function badLayout() {
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
const height = el.offsetHeight; // Read
el.style.height = height + 10 + 'px'; // Write
});
}
// ✅ Batch reads then batch writes
function goodLayout() {
const elements = document.querySelectorAll('.item');
const heights: number[] = [];
// Batch reads
elements.forEach(el => {
heights.push(el.offsetHeight);
});
// Batch writes
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});
}
// ✅ Use requestAnimationFrame
function animateWithRAF() {
requestAnimationFrame(() => {
// Execute DOM updates in next frame
element.style.transform = 'translateX(100px)';
});
}
React Rendering Optimization
// Use memo to avoid unnecessary re-renders
const ExpensiveComponent = memo(function ExpensiveComponent({ data }: Props) {
return <div>{/* Complex rendering logic */}</div>;
});
// useMemo to cache computed results
function DataTable({ items }: { items: Item[] }) {
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
return <Table data={sortedItems} />;
}
// useCallback to cache callback functions
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <Child onClick={handleClick} />;
}
// Use React.lazy and Suspense
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>
);
}
Caching Strategies
Service Worker Caching
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
];
// Cache static assets on install
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
});
// Network-first strategy
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(networkFirst(event.request));
} else {
event.respondWith(cacheFirst(event.request));
}
});
async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
}
async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
throw error;
}
}
HTTP Caching
// Express cache header setup
app.use('/static', express.static('public', {
maxAge: '1y',
immutable: true,
}));
app.get('/api/data', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
'ETag': generateETag(data),
});
res.json(data);
});
// Versioned asset URLs
const assetUrl = `/app.${hash}.js`;
Browser Storage
// IndexedDB for caching large data
import { openDB } from 'idb';
const db = await openDB('app-cache', 1, {
upgrade(db) {
db.createObjectStore('api-cache', { keyPath: 'url' });
},
});
async function cachedFetch(url: string): Promise<Response> {
const cached = await db.get('api-cache', url);
if (cached && Date.now() - cached.timestamp < 300000) {
return new Response(JSON.stringify(cached.data));
}
const response = await fetch(url);
const data = await response.json();
await db.put('api-cache', {
url,
data,
timestamp: Date.now(),
});
return new Response(JSON.stringify(data));
}
Network Optimization
HTTP/2 and HTTP/3
# Nginx HTTP/2 configuration
server {
listen 443 ssl http2;
# Server push
location / {
http2_push /styles.css;
http2_push /app.js;
}
}
Compression
// Express compression middleware
import compression from 'compression';
app.use(compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
}));
// Brotli compression (better ratio)
import shrinkRay from 'shrink-ray-current';
app.use(shrinkRay());
Resource Hints
// Dynamic resource hints
function addResourceHint(type: 'preload' | 'prefetch', url: string) {
const link = document.createElement('link');
link.rel = type;
link.href = url;
document.head.appendChild(link);
}
// Preload based on user behavior
function onHover(event: MouseEvent) {
const link = (event.target as HTMLElement).closest('a');
if (link?.href) {
addResourceHint('prefetch', link.href);
}
}
Performance Monitoring
Custom Performance Metrics
// Custom performance marks
performance.mark('app-init-start');
await initializeApp();
performance.mark('app-init-end');
performance.measure('app-init', 'app-init-start', 'app-init-end');
// Get measurement results
const measures = performance.getEntriesByType('measure');
console.log('App init time:', measures[0].duration);
// Long task monitoring
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long task detected:', entry);
reportLongTask(entry);
}
}
});
observer.observe({ entryTypes: ['longtask'] });
Error and Crash Monitoring
// Global error capture
window.addEventListener('error', (event) => {
reportError({
type: 'uncaught',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
// Promise error capture
window.addEventListener('unhandledrejection', (event) => {
reportError({
type: 'unhandledrejection',
reason: event.reason,
});
});
// Memory monitoring
if ('memory' in performance) {
setInterval(() => {
const memory = (performance as any).memory;
if (memory.usedJSHeapSize > memory.jsHeapSizeLimit * 0.9) {
reportMemoryWarning(memory);
}
}, 30000);
}
Build Optimization
// Vite configuration optimization
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'date-fns'],
},
},
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
});
Best Practices Summary
Frontend Performance Best Practices:
┌─────────────────────────────────────────────────────┐
│ │
│ Loading Optimization │
│ ├── Code splitting and lazy loading │
│ ├── Resource compression and optimization │
│ ├── CDN distribution │
│ └── Preload critical resources │
│ │
│ Rendering Optimization │
│ ├── Virtual list for large data │
│ ├── Avoid layout thrashing │
│ ├── CSS animations over JS │
│ └── Optimize React rendering │
│ │
│ Caching Strategies │
│ ├── Service Worker caching │
│ ├── HTTP cache headers │
│ ├── Local storage caching │
│ └── API response caching │
│ │
│ Monitoring System │
│ ├── Core Web Vitals │
│ ├── Custom performance metrics │
│ ├── Error monitoring │
│ └── User experience monitoring │
│ │
└─────────────────────────────────────────────────────┘
| Metric | Target | Optimization Focus |
|---|---|---|
| LCP | < 2.5s | Optimize first-screen resources |
| INP | < 200ms | Reduce JS execution |
| CLS | < 0.1 | Reserve space |
| FCP | < 1.8s | Reduce blocking resources |
| TTFB | < 800ms | Optimize server response |
Performance optimization is an ongoing process. Establish monitoring systems and continuously iterate to maintain great user experience.
Performance is the foundation of user experience. Every millisecond matters for user retention.