This article is published in English.
Measure First: Why Early Optimization Makes Next.js Apps Harder to Run
See how premature memoization, broad client boundaries and stacked caches add complexity to Next.js apps, and how a measurement-first workflow keeps them fast.
A Next.js page loads in 1.2 seconds, someone decides that is too slow, and a round of optimization begins before anyone has looked at a profile. A few months later the codebase has memoization everywhere, dynamic imports nobody benchmarked, several overlapping caches and custom rendering paths, and the app is harder to understand than it ever was slow. This guide explains why that pattern is so common, which specific habits cause it, and how to replace them with a workflow that starts from evidence and adds complexity only when the numbers justify it.
The real mistake: complexity before evidence
Each individual optimization usually looks sensible in a code review. A useMemo here, a cache there, a split bundle for a component that seemed heavy. The trouble is cumulative. Every mechanism adds caching behavior to understand, another rendering path to debug, one more boundary to reason about and more performance-specific code to keep working.
The failure worth guarding against is therefore not a lack of optimization. It is introducing machinery before you know what is slow, why it is slow and whether fixing it would change anything a user notices. Before tuning execution, make the system observable enough that you can measure it.
Profile before you change anything
The most common version of this mistake is reacting to the general idea that performance matters. A developer starts editing code without profiling, without isolating a bottleneck and without confirming that users are waiting on anything in particular.
The symptoms are familiar:
useMemoanduseCallbackwrapped around cheap calculations- caching layers added for data that was never expensive to fetch
- code splitting applied before anyone checked which chunks were large
- abstractions built around hypothetical future load
- rendering logic that grows conditions to avoid renders nobody measured
Frequently the original problem was barely there. Real performance work begins with data: page timings, a bundle analysis, a React profiler recording, the network waterfall and a clear picture of where users actually wait. Optimization should answer an observed behavior, not a vague worry about what might become slow someday.
A practical rule of thumb is to write down the metric you intend to move and its current value before touching code. If you cannot name the number, you are not ready to change the implementation.
Usually the problem is simply too much JavaScript
A lot of frontend slowness is not mysterious. The browser is being asked to download, parse, compile and execute more script than the page needs, and on a mid-range phone each of those steps is expensive.
A modest dashboard can quietly accumulate a couple of animation libraries, a large component kit, a heavyweight state manager, charting packages, utility collections and client-side logic for features that could have stayed on the server. None of them looks alarming alone. Together they create a large amount of work before the page responds comfortably to input. At that point teams often start chasing poor Lighthouse numbers as if the fix required a clever strategy.
Next.js already gives you strong defaults here: server rendering, automatic code splitting per route, and a server-first model built on React Server Components. Those defaults lose much of their value when large parts of the app are pushed into the browser anyway. So before reaching for another technique, check how much code you ship, which dependencies dominate each route and whether each one earns its weight. Many applications do not need more sophisticated optimization; they need less JavaScript. For a deeper look at what else besides raw bundle size can slow a page, see finding what actually makes a web app slow.
Client boundaries that creep upward
The quickest way to give away the architectural benefits of the App Router is to mark too much as client code. Someone needs interactivity deep in a tree, puts "use client" on a parent, then on a grandparent, and soon components that only render static markup, read server data or compose layout are part of the client bundle simply because they sit below that directive.
That shift affects more than where code runs. It usually means:
- more JavaScript sent to the browser
- more hydration work before the page is interactive
- additional client-side state to manage
- more places where server and client data can drift out of sync
There is an irony in this. Many teams move to modern Next.js precisely to get a server-first architecture, then gradually rebuild the client-heavy single-page app they were trying to leave.
A useful question during design is: what is the smallest piece of this interface that truly needs the browser? A like button, a dropdown or a form input may need client state; the card, list and page around it often do not. Keeping the boundary tight, and passing server-rendered content into client components as children where possible, lets the server do more work while interactive islands stay focused. The long-term payoff is smaller bundles and a simpler mental model. The mechanics behind this are covered in how React Server Components keep code out of the bundle.
How premature optimization makes architecture fragile
Performance work becomes a maintenance problem when optimizations arrive faster than anyone can show they help. It tends to start small: a component is memoized, a hand-rolled cache appears, a hook is added to skip a render, then another layer shows up to keep two pieces of state consistent. Nothing looks risky at the time.
Months later the codebase contains custom hooks whose interactions are hard to trace, invalidation rules only a couple of people understand, chains of memoized values, render conditions based on assumptions that no longer hold, and synchronization code that exists mainly because some earlier optimization required it.
That has a real cost. Onboarding takes longer, debugging needs more context, and small feature changes end up touching mechanisms that were originally added for speed. The right question about any optimization is not whether it improves a benchmark. It is whether the improvement is large enough to pay for the architectural cost it introduces. The target is sustainable performance: an app that responds quickly while its everyday code stays plain and readable rather than packed with speed tricks.
Perceived performance is a UX problem
Engineers gravitate toward what can be measured precisely, such as milliseconds, bundle sizes, render counts and scores. Users experience something wider. Shaving 100ms off a page load does little if navigation is confusing, loading states give no feedback, controls feel dead, the layout jumps as content arrives or an important action shows no sign that it registered.
Take a form that takes two seconds to submit. Cutting the backend time to 1.7 seconds is a genuine technical gain. Showing immediate feedback, disabling the button to prevent a duplicate submission and displaying clear progress will often improve the experience far more, even though the request is exactly as slow as before.
That is the gap between measured and perceived performance. People judge an interface by whether it responds to what they intend, whether they understand what is happening and whether it feels stable as they move through it. Good frontend performance work therefore draws on interaction design as much as on rendering internals. Tools such as React transitions, optimistic updates and skeleton states belong in the same toolbox as bundle analysis.
Caching helps until nobody can explain it
Caching can produce large gains because the system stops repeating expensive work. The difficulty starts when the team can no longer say which version of the data a given user should be seeing.
The typical story: a page gets fast, then stale data appears. A record is edited and one screen updates while another keeps showing the old value. Development behaves correctly and production does not, and the investigation turns into questions about which cache served the response, which layer was invalidated and which request produced the output.
Large Next.js applications make this especially easy because reuse can happen at many levels: your own code, the framework's data and route caching, individual fetch calls, a CDN, the browser and backend services. Adding another layer without understanding how those interact may reduce latency while multiplying the number of states the system can be in. Note that Next.js caching defaults have changed across major versions, so confirm the behavior of the version you run in the current documentation rather than relying on older guides.
Observability should come before aggressive caching. For any cached response, the team should be able to answer:
- where the response came from
- how long it is expected to stay valid
- what invalidates it
- what happens when invalidation fails
A cache that speeds the system up but makes production behavior unpredictable is not a free win.
The slowness may not be in React at all
Sometimes the frontend is only where a delay becomes visible, not where it starts. Faced with a view that needs a few seconds before it is usable, a team may start trimming renders, memoizing components or restructuring client state. Those changes might save a few milliseconds of browser work while the page still waits on a two-second database query or an endpoint that returns far more data than the view uses.
Picture the lifecycle of that request: the browser sends it, it passes through middleware and authorization, the server calls a backend or database, the payload travels back, and only then does React render. If most of the wall-clock time is spent before the response arrives, making the last step slightly faster cannot help much.
The same applies to oversized payloads, sequential network calls that could run in parallel, expensive authorization checks, overloaded services and unindexed queries. None of these is fixed by rendering a component a little less often. A useful investigation follows the whole request and asks where the time goes. Bottlenecks do not respect team boundaries or who owns the code.
Simple systems stay fast longer
Many fast applications are unremarkable on the inside. They ship reasonably small bundles, draw clear rendering boundaries, keep client state minimal, fetch data predictably and have an architecture a new developer can follow without reverse-engineering a web of tricks.
That plainness pays off as the app grows. When data flow is obvious, expensive work is easy to spot. When client boundaries are narrow, it is clear what the browser is responsible for. When caching rules are few and explicit, production issues are easier to diagnose.
Clever optimization is attractive partly because it shows technical skill, but every mechanism becomes something future developers must understand, debug, preserve or eventually remove. None of this argues against optimizing. It argues for the simplest implementation that meets the real performance requirement, with added complexity only once measurements show the simple design has run out of room. A slightly less clever system that is much easier to reason about usually ages better.
Treat scores as signals, not goals
Benchmark tools are valuable because they make invisible characteristics inspectable. The problem begins when raising the score matters more than improving the product.
A strong Lighthouse result does not guarantee good interaction design, maintainable architecture, reliable production behavior or fast completion of the tasks users care about. Lab measurements run under controlled assumptions; real visitors bring different devices, networks, data volumes, authentication states and navigation paths. Field data, such as Core Web Vitals collected from real sessions, is a useful complement for exactly that reason.
This does not make lab metrics unimportant; it changes how you use them:
- If a metric points to a genuine problem, investigate it.
- If a change raises the score but adds significant complexity while users notice almost nothing, question the trade-off.
- Keep every metric tied to a user-facing behavior you can describe.
The aim is not an app that looks great inside a benchmark. It is an app that lets people get their work done without needless delay or friction.
A measurement-first workflow
Pulling these ideas together, a sustainable loop looks like this:
- Name the user-facing problem and the metric that represents it.
- Measure the current value in both lab and, where possible, field conditions.
- Trace the whole request and render path to locate the dominant cost.
- Try the change that removes work first: fewer dependencies, a narrower client boundary, a smaller payload, a faster query.
- Reach for memoization, extra caching or custom rendering only if removal is not enough.
- Measure again and keep the change only if the gain is worth its maintenance cost.
Put the client boundary on the leaf
The premature move is a 'use client' directive on the page, which drags the data fetch into the browser bundle. Leave the page as a Server Component and mark only the component that needs events or browser APIs.
// app/dashboard/page.tsx — a Server Component, no 'use client'
import { Chart } from './chart';
export default async function Dashboard() {
const points = await loadSeries();
return <Chart points={points} />;
}
If a profile later shows that rendering, not the network, dominates, memoize that leaf. Until the profile says so, the narrower boundary is the optimization.
'use client';
export const Chart = ({ points }: { points: number[] }) => {
return <svg data-count={points.length} />;
};
Key takeaways
- The costliest Next.js performance mistake is optimizing before understanding what the system is doing, not forgetting to optimize.
- Most maintainability damage comes from accumulated complexity: excess client code, stacked caches, avoidable hydration and speculative abstractions.
- Removing work usually beats adding mechanisms, whether that means shipping less JavaScript, keeping components on the server, simplifying state, dropping redundant requests, fixing a slow backend call or deleting an optimization that costs more than it saves.
- Some applications genuinely need sophisticated caching or rendering strategies, but that decision should follow measurement and a clear diagnosis.
- The fastest application is often the one doing the least unnecessary work.