Performance
Loading Performance
Bundle optimization, lazy loading, caching strategies, and resource prioritization
Loading Performance
Loading performance determines how quickly users can see and interact with your application. This involves optimizing bundle size, resource loading, and caching.
Bundle Size Optimization
Code Splitting
// Route-based splitting
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
);
}
// Component-based splitting
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const MarkdownEditor = lazy(() =>
import('./components/MarkdownEditor').then(mod => ({ default: mod.Editor }))
);Tree Shaking
// Bad - imports entire library
import _ from 'lodash';
_.debounce(fn, 300);
// Good - imports only what's needed
import debounce from 'lodash/debounce';
debounce(fn, 300);
// Better - use lodash-es for ES modules
import { debounce } from 'lodash-es';
// Best - use native or smaller alternatives
function debounce(fn, ms) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), ms);
};
}Analyzing Bundle Size
# Using vite-plugin-visualizer
pnpm add -D rollup-plugin-visualizer
# Using webpack-bundle-analyzer
pnpm add -D webpack-bundle-analyzer
# Using source-map-explorer
npx source-map-explorer dist/assets/*.js// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
filename: 'bundle-stats.html',
gzipSize: true,
brotliSize: true,
}),
],
});Resource Loading Strategies
Resource Hints
<head>
<!-- Preconnect to required origins -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<!-- DNS prefetch for future navigations -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/hero-image.webp" as="image">
<!-- Prefetch next page resources -->
<link rel="prefetch" href="/next-page.js">
<!-- Prerender entire page (use sparingly) -->
<link rel="prerender" href="/likely-next-page">
</head>Script Loading Strategies
<!-- Regular - blocks parsing -->
<script src="app.js"></script>
<!-- Async - downloads in parallel, executes immediately when ready -->
<script async src="analytics.js"></script>
<!-- Defer - downloads in parallel, executes after DOM parsing -->
<script defer src="app.js"></script>
<!-- Module - automatically deferred -->
<script type="module" src="app.js"></script>Critical CSS
// Extract and inline critical CSS
// Using critters with Vite
import critters from 'critters-webpack-plugin';
// Or manually inline
const criticalCSS = `
/* Only styles needed for above-the-fold content */
body { margin: 0; font-family: system-ui; }
.header { height: 64px; }
.hero { min-height: 400px; }
`;
// In HTML
<style>${criticalCSS}</style>
<link rel="preload" href="/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">Image Optimization
Modern Formats
<picture>
<!-- AVIF for best compression -->
<source srcset="image.avif" type="image/avif">
<!-- WebP for wide support -->
<source srcset="image.webp" type="image/webp">
<!-- JPEG fallback -->
<img src="image.jpg" alt="Description" loading="lazy">
</picture>Responsive Images
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w,
image-1600.jpg 1600w
"
sizes="
(max-width: 400px) 100vw,
(max-width: 800px) 50vw,
33vw
"
alt="Responsive image"
loading="lazy"
decoding="async"
>Lazy Loading
// Native lazy loading
<img src="image.jpg" loading="lazy" alt="Lazy loaded">
// Intersection Observer for more control
function useLazyLoad(ref) {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsLoaded(true);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [ref]);
return isLoaded;
}Caching Strategies
HTTP Caching
# nginx.conf
location /assets {
# Immutable assets with content hash
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /index.html {
# HTML should always be revalidated
add_header Cache-Control "no-cache";
}
location /api {
# API responses - short cache with revalidation
add_header Cache-Control "private, max-age=0, must-revalidate";
}Service Worker Caching
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/main.js',
'/main.css',
];
// Install - cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
});
// Fetch - stale-while-revalidate strategy
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
const networked = fetch(event.request).then((response) => {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
return response;
});
return cached || networked;
})
);
});React Query / SWR Caching
// TanStack Query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes
refetchOnWindowFocus: false,
retry: 3,
},
},
});
// SWR
const { data } = useSWR('/api/user', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: true,
dedupingInterval: 60000,
});Compression
Build-time Compression
// vite.config.ts
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
compression({
algorithm: 'gzip',
ext: '.gz',
}),
compression({
algorithm: 'brotliCompress',
ext: '.br',
}),
],
});Server Configuration
# nginx.conf
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# Brotli (requires ngx_brotli module)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript;Fonts Optimization
/* Preload critical fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>Font Loading Strategies
| Strategy | Behavior | Use Case |
|---|---|---|
swap | Show fallback, swap when loaded | Body text |
optional | Use if cached, skip if slow | Non-critical text |
fallback | Brief invisible, then fallback | Balance |
block | Invisible until loaded | Icon fonts |
Best Practices
Loading Performance Guidelines
- Keep initial bundle under 200KB (gzipped)
- Use code splitting for routes and heavy components
- Implement proper caching with content hashes
- Preload critical resources, prefetch likely next pages
- Optimize images with modern formats and lazy loading
- Compress all text-based assets
- Use CDN for static assets
- Monitor and set performance budgets