Frontend Optimization: The Techniques That Actually Pay Off
A practical tour of the optimization techniques I reach for, ordered by impact. Ship less JavaScript, load it smarter, render less often, and measure everything
Frontend optimization has a fashion problem. Every year there is a new trick that gets blogged to death, and teams adopt it while ignoring the boring fundamentals that would have mattered ten times more. I have watched teams add elaborate memoization to a page whose real problem was a 2 MB JavaScript bundle nobody had looked at.
So here is my working list, roughly ordered by impact. The order matters more than any individual item. Do the top of the list first, measure, and only move down when the data says to.
First rule: measure before you touch anything
Optimization without measurement is superstition. Before changing code, profile. The browser gives you everything you need: the Performance panel for runtime work, the Network panel for what you are shipping, the Coverage tab for how much of it goes unused, and the bundle analyzer for where the weight is.
The goal of measuring first is to find the one thing that dominates, because performance problems are almost never evenly distributed. There is usually a single fat dependency, a single unbatched render, or a single blocking request doing most of the damage. Find that, fix that, remeasure. Spreading effort evenly across a codebase is how you spend a week to save 40 milliseconds.
The biggest lever: ship less JavaScript
For most sites, the amount of JavaScript is the performance story. It has to be downloaded, parsed, and executed, and on a mid-tier phone that execution cost is brutal. Every other technique is a rounding error next to shipping less of it.
Code splitting. Do not ship your whole app to render one route. Split by route so users download the code for the page they are on, then split heavy components (a rich text editor, a charting library, a date picker) behind dynamic imports so they load only when actually used.
Before: one bundle
[########## 850 KB ##########] downloaded on every page
After: split by route + lazy chunks
[## core 120 KB ##] every page
+ [# checkout 90 KB #] only on /checkout
+ [# editor 140 KB #] only when the editor opens
Tree shaking and honest imports. Import what you use, not the world. import { debounce } from 'lodash' can pull the entire library if the bundler cannot tree-shake it, while import debounce from 'lodash/debounce' pulls one function. Audit your heaviest dependencies and check whether a lighter one exists. A single date or moment library can outweigh your entire application code.
Question every dependency. The cheapest kilobyte is the one you never ship. A 30-line utility you write beats a 40 KB package that does the same thing. Before adding a dependency, check its cost on a bundle size tool.
Load what you keep, intelligently
Once the payload is lean, control the order and timing of loading.
Prioritize the critical path. The browser needs the right resources in the right order to render what the user sees first. Preload the hero image and the fonts that matter, defer the rest. Inline the small amount of critical CSS so first paint does not wait on a stylesheet round trip.
Lazy load below the fold, eagerly load above it. Images and components the user cannot see yet should not compete for bandwidth with what they can. But never lazy load your hero image or above-the-fold content, that is a self-inflicted delay I see constantly.
Defer non-essential third parties. The analytics tag, the chat widget, the tag manager. They rarely need to run before the page is interactive. Load them after, or off the main thread entirely with a tool like Partytown, so they stop competing with your own code for the CPU.
Render less, and less often
After the network and bundle, the next cost is the framework doing work it did not need to do.
Do not memoize by reflex. memo, useMemo, and useCallback are not free, and sprinkling them everywhere adds overhead and noise while often fixing nothing. Profile first, find the component that actually re-renders too much or does expensive work, and apply memoization there with intent. Note that newer framework compilers are starting to handle a lot of this automatically, which is another reason not to hand-optimize blindly.
Virtualize long lists. Rendering 5,000 rows into the DOM is slow to paint and slow to interact with, no matter how clean your components are. Render only the handful of rows in the viewport with a windowing library. This is one of the highest-leverage fixes for any data-heavy interface.
Keep the main thread free. Long tasks over 50 milliseconds block every interaction that lands during them. Break big synchronous work into chunks, yield back to the browser, and move genuinely heavy computation (parsing, image processing, crypto) into a Web Worker so the UI stays responsive.
Serve it well
The best-optimized frontend still feels slow behind a slow server or a poor cache.
Cache aggressively and correctly. Fingerprinted static assets should be cached effectively forever, since a new build produces a new filename. HTML and API responses need a deliberate caching policy. A CDN in front of your assets puts them physically close to users and is one of the cheapest wins available.
Pick the right rendering strategy per route. Not everything needs to be a client-rendered SPA. A marketing page wants static generation, a dashboard wants server rendering with streaming, and truly static content wants to be static. Match the strategy to the page instead of applying one approach to the whole app.
Optimize images, because they are usually the heaviest thing on the page. Correct dimensions, modern formats like AVIF or WebP, and a real responsive srcset so a phone does not download a desktop-sized image. An unoptimized hero image can outweigh your entire JavaScript bundle.
Guard the wins in CI
The uncomfortable truth is that optimization decays. You spend a week getting the bundle lean, and six months later it is heavier than when you started, because every feature since added weight and nobody was watching.
Put a performance budget in CI and fail the build when a pull request blows past it. A budget that only lives in a dashboard is ignored. A budget that blocks a merge gets respected. This is the difference between a one-time cleanup and a codebase that stays fast.
The short version
Measure first and find the one thing that dominates. Ship less JavaScript, because it is the biggest lever by far. Load what remains in the right order. Render less and less often, but only where profiling tells you to. Serve it with good caching and the right rendering strategy per route. Then put a budget in CI so none of it silently erodes.
Notice how little of this is clever. Performance work is mostly discipline: doing the boring high-impact things in the right order and refusing to be distracted by the fashionable low-impact ones.
Building something in this space?
I take on select builds when the work is worth doing right.