The JavaScript You Don't Ship Is Always the Fastest

Frontend performance isn't about squeezing milliseconds from your render cycle. It's about rethinking what you're actually sending to the browser — and why.
There's a conversation that comes up in almost every frontend team at some point. A stakeholder opens Lighthouse, sees a score in the 50s, and suddenly performance is a priority. What follows is usually a scramble: compress images, add lazy loading, throw in a CDN, and ship it. The score goes up. Everyone moves on.
The problem is that approach treats performance like a checklist, and checklists don't scale. Six months later, the score is back in the 50s, and no one quite understands why. The root cause is almost always the same: the team optimized the symptoms without ever addressing what was actually making the app slow.
After spending years on performance-critical React applications, I've come to a fairly simple conclusion: most frontend performance problems are really JavaScript problems. Not "bad" JavaScript necessarily, just too much of it, loaded at the wrong time, doing work the browser already knows how to do.
Most performance problems aren't about how you write your code. They're about how much of it you force the user to download before they can do anything.
The cost of JavaScript nobody talks about
When we talk about JavaScript performance, the conversation usually goes straight to execution time. But parse and compile time — the work the browser does before a single line of your code runs — is often a bigger bottleneck, especially on mid-range Android devices.
Google's data on this is worth sitting with: a 170KB compressed JavaScript bundle takes roughly 0.7 seconds to parse on a modern desktop. On a mid-range phone from 2021, that same bundle can take over 3 seconds. The file size is the same. The user experience is wildly different.
This is why the most effective performance optimization often isn't faster code — it's less code. And the most reliable way to ship less code is to be deliberate about what you import and when.
- Third-party libraries imported wholesale instead of by method (lodash, moment, etc.)
- UI component libraries that aren't tree-shaken properly
- Heavy utilities loaded on routes that don't need them
- Duplicate dependencies pulled in by different packages at different versions
Code splitting isn't optional anymore
React's lazy() and Suspense have been around long enough that there's no real excuse not to use them. Yet I still see production apps shipping a single monolithic bundle that includes the admin dashboard, the onboarding flow, the chart library, and the rich text editor — all of it, on the landing page, before the user has even logged in.
The fix is conceptually simple: load code when it's needed, not when the app initializes. In practice that means auditing your routes and identifying what's heavy and conditional. An editor that only power users open? Lazy load it. A date picker that only appears in one modal? Lazy load it. The analytics dashboard behind an authenticated route? Absolutely lazy load it.
The gains here aren't marginal. On a recent project, moving four heavy components to lazy imports cut the initial bundle by 38%. That translated directly to a 1.2 second improvement in Time to Interactive on a throttled 3G connection — which is closer to what a significant portion of mobile users actually experience.
Re-renders: the silent performance tax
Bundle size gets most of the attention, but unnecessary re-renders are the other major source of frontend sluggishness — and they're harder to spot because nothing breaks. The app just feels slightly off. Interactions have a lag you can't quite pinpoint. Animations stutter on pages that shouldn't have any performance issues at all.
The usual culprit is state that lives too high in the component tree. When a piece of state updates — say, a search input value — React re-renders every component that's subscribed to that state, including ones that have nothing to do with the search. In a small app this barely matters. In a complex UI with dozens of components, it adds up fast.
The fix is often just moving state down. Before reaching for memo() or useMemo(), ask whether the problem is better solved by restructuring the component tree. Memoization is a useful tool, but it has overhead too, and it adds cognitive complexity to code that could have been simple.
The metrics that actually matter
Lighthouse is a useful proxy, but it's a proxy. The number that actually correlates with user behavior — and ultimately with conversion, retention, and revenue — is Interaction to Next Paint (INP), which became a Core Web Vital in 2024. INP measures the latency of all interactions throughout the user's session, not just the initial load.
This shifts the performance conversation from "how fast does the page load?" to "how responsive does the page feel while someone is using it?" Those are very different questions, and they often point to different problems. A page can load quickly and still feel sluggish because long tasks on the main thread are blocking user input.
INP doesn't care about your Lighthouse score. It cares about what happens when a real user clicks a button and waits for something to happen.
Diagnosing INP issues usually means profiling with Chrome DevTools and looking for long tasks — anything over 50ms that runs on the main thread. The fix is typically one of three things: deferring non-critical work with requestIdleCallback, breaking up heavy computations with scheduler.postTask, or moving CPU-intensive work off the main thread entirely with a Web Worker.
What good performance work actually looks like
The teams that maintain consistently fast frontends don't treat performance as a project. They treat it as a constraint that exists from the start. That means setting bundle size budgets and failing CI when they're exceeded. It means reviewing Lighthouse and Web Vitals data alongside feature metrics, not as a separate concern. It means asking "what's the performance cost?" during architecture discussions, before the code is written.
None of this is complicated. But it requires a shift in how you think about what "done" means. A feature isn't done when it works. It's done when it works without making the rest of the experience slower.
Conclusion
Frontend performance is one of those areas where the fundamentals age incredibly well. The specifics change — new metrics, new APIs, new tooling — but the underlying principles don't: ship less, load late, keep the main thread free. Those three things, applied consistently, will get you further than any optimization trick.
The teams I've seen build genuinely fast products aren't doing anything exotic. They're just disciplined about the basics, and they've made performance a habit rather than a fire drill. That's the part that doesn't show up in any article about tools or frameworks, but it's the part that actually makes the difference.