Improve Core Web Vitals, reduce bundle size, optimize images, and make your web app faster with proven techniques used by top-performing sites.
Performance is a feature. A 1-second delay in page load time causes a 7% drop in conversions. Here's how to make your web app fast — measurably, not just subjectively.
| Metric | What it measures | Good | Needs work | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Load time of main content | <2.5s | 2.5–4s | >4s |
| INP (Interaction to Next Paint) | Response to user input | <200ms | 200–500ms | >500ms |
| CLS (Cumulative Layout Shift) | Visual stability | <0.1 | 0.1–0.25 | >0.25 |
Measure first with PageSpeed Insights — fix what actually moves the needle.
Images often account for 50–70% of page weight.
// Next.js — use next/image for automatic optimization
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={630}
priority // load immediately (for above-fold images)
sizes="(max-width: 768px) 100vw, 1200px" // responsive
/>
// next/image automatically:
// ✓ Converts to WebP/AVIF
// ✓ Generates srcset for responsive sizes
// ✓ Lazy-loads below-fold images
// ✓ Prevents layout shift with width/height
Format guidance:
# Analyze your Next.js bundle
npm install @next/bundle-analyzer
ANALYZE=true npm run build
Common bundle killers and fixes:
// ✗ Importing entire library
import _ from 'lodash';
_.debounce(fn, 300);
// ✓ Import only what you need
import debounce from 'lodash/debounce';
// ✗ Import moment (300KB)
import moment from 'moment';
// ✓ Use date-fns (tree-shakeable) or built-in Intl
import { format } from 'date-fns';
new Intl.DateTimeFormat('en-US').format(new Date());
// ✓ Dynamic import for heavy components
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <Skeleton />,
ssr: false,
});
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>
<link rel="preload" href="/hero-image.webp" as="image">
<!-- Preconnect to external origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://api.yourservice.com">
<!-- Defer non-critical scripts -->
<script src="analytics.js" defer></script>
<script src="chat-widget.js" async></script>
/* Prevent FOUT (Flash of Unstyled Text) */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* show fallback while loading */
font-display: optional; /* skip if not loaded quickly (best for CLS) */
}
In Next.js, always use next/font — it handles subset loading, zero layout shift, and self-hosting automatically:
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
# Static assets (JS, CSS, fonts) — long cache + content hash
Cache-Control: public, max-age=31536000, immutable
# HTML pages — short cache or no cache
Cache-Control: public, max-age=0, must-revalidate
# API responses — stale while revalidate
Cache-Control: public, s-maxage=60, stale-while-revalidate=3600
The server doesn't matter if your queries are slow:
-- Add indexes on columns used in WHERE, JOIN, ORDER BY
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published_at ON posts(published_at DESC)
WHERE draft = false; -- partial index
-- Use EXPLAIN ANALYZE to find slow queries
EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 1 ORDER BY created_at DESC;
-- Select only columns you need
SELECT id, title, published_at FROM posts -- not SELECT *
next/image or equivalent for all imageswidth/height to all <img> tags to prevent CLS