Boring Guide to 10x Frontend Performance
writingEvery developer who has shipped a production frontend eventually hits the moment where the app feels slow and they have no idea why. I have been there more than once. After auditing my own projects and diving into the Lighthouse reports of sites I was building at Abhibus, a pattern kept showing up: the biggest wins were almost never in the JavaScript. They were in the boring stuff that everyone skips.
The Bundle Size Problem
Unused code is probably responsible for a larger share of your bundle than you think. A typical React app imports a full UI library to use three components. It ships development-mode warnings. It includes every locale string for a date library when you only need English. The fastest way to audit this is to run npx @next/bundle-analyzer or the webpack bundle analyzer and look for anything suspiciously large. You will almost always find something that can be swapped for a smaller alternative or lazy-loaded.
Code splitting with Next.js dynamic imports
import dynamic from 'next/dynamic';
// Heavy chart library only loads when the component is actually rendered
const Chart = dynamic(() => import('./HeavyChart'), {
loading: () => <div className="h-64 bg-zinc-100 animate-pulse rounded" />,
ssr: false,
});Memory Leaks in React
The most common memory leak I have seen is a missing cleanup in a useEffect. You set up an interval or subscription, but you never return a cleanup function. The component unmounts but the timer keeps running, keeping references to stale state alive. If this happens in a list that mounts and unmounts a lot you will see memory grow over time.
Missing cleanup vs correct cleanup
// Leaks memory — interval keeps running after unmount
useEffect(() => {
const interval = setInterval(() => {
setData(prev => [...prev, fetchLatest()]);
}, 1000);
// Missing return
}, []);
// Correct — cleanup runs on unmount
useEffect(() => {
const interval = setInterval(() => {
setData(prev => [...prev.slice(-100), fetchLatest()]);
}, 1000);
return () => clearInterval(interval);
}, []);Network: The Biggest Win Nobody Does
Setting proper cache headers on your static assets is one of the highest-leverage performance changes you can make and it takes about five minutes. Images and fonts that change rarely should have a very long max-age. JavaScript bundles built with content hashes can be cached indefinitely because the URL changes whenever the content changes. Most developers never set these and wonder why their Lighthouse score shows poor cache policy.
vercel.json cache configuration
{
"headers": [
{
"source": "/static/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" }
]
}
]
}Measure Before Optimizing
The most important rule is one that gets ignored most often. Run Lighthouse or WebPageTest on your app before touching anything. Find the actual bottleneck. Nine times out of ten it is not what you assumed it was. I have spent hours micro-optimizing a React component tree when the real problem was a 400KB image served without compression. Do not optimize what you have not measured.
What these changes actually look like
Applied together across a typical mid-sized React app, the improvements compound. Bundle analysis and code splitting handle the load time. Proper cleanup prevents memory from growing unchecked in long sessions. Cache headers mean returning users get instant loads from the second visit onward. None of these are exciting. They are just the fundamentals done right.
Performance Results
Where to start
Open Lighthouse right now on your production site. Look at the three lowest-scoring sections. Fix those first. Then look at your bundle with a bundle analyzer. Remove or lazy-load anything you can. Check your cache headers. These three steps will get you most of the way there without touching a single component.