RankSoft logo

Published on July 12, 2026

← Browse Articles

Next.js Performance

Why Next.js Projects Become Slow Over Time

Most Next.js projects do not start slow. They usually start with a clean structure, a small number of pages, a limited amount of client JavaScript, and a backend that still responds quickly. The first version feels modern, simple, and fast.

The problem appears later. More pages are added. More developers work on the same codebase. Marketing scripts are introduced. Product teams request personalization. Designers add interactive elements. Backend APIs grow. Database queries become heavier. Suddenly, the same application that once felt instant starts to feel expensive to run and slow to use.

This is one of the most common production problems in modern web applications. Next.js gives teams powerful tools, but it does not automatically prevent architectural decay. Performance must be treated as an engineering discipline, not as a final polishing step before release.

Next.js Is Not Usually the Real Problem

When a Next.js project becomes slow, the first reaction is often to blame the framework. In practice, the framework is rarely the main problem. The real problem is usually the way the application evolved.

A slow production Next.js application is often a combination of many small decisions. One unnecessary client component is not a disaster. One extra API request is not a disaster. One analytics script is not a disaster. One unoptimized image is not a disaster. But after six months, those decisions become a system-wide performance problem.

Good performance work starts by separating symptoms from causes. A page may feel slow because React hydration is expensive, but hydration may be expensive because too much of the page was moved to the client. A server-rendered page may respond slowly, but the real issue may be a database query behind an API call. A loading spinner may appear late, but the actual problem may be a request waterfall.

The Hidden Cost of Too Many Client Components

One of the most common reasons Next.js applications become slow is the uncontrolled growth of client components. The directive looks small, but its impact can be large.

'use client'

Once a component becomes a client component, it must be shipped to the browser as JavaScript. The browser then has to download it, parse it, execute it, and hydrate it. On a powerful development machine this may feel acceptable. On a slower mobile device, it can become one of the biggest user-facing performance costs.

The dangerous pattern is not using client components. They are necessary for interactivity. The dangerous pattern is using them as the default. A navigation menu, filter panel, gallery, pricing calculator, or booking widget may need client-side behavior. But the surrounding layout, static text, SEO content, headings, and most product information usually do not.

A healthy Next.js codebase keeps client components small and isolated. It treats the browser as an expensive runtime, not as a place where every part of the page should automatically execute.

Server Rendering Does Not Fix Slow Data Fetching

Server rendering can improve perceived performance, SEO, and initial content delivery. But server rendering does not make slow data sources fast. If a page waits for slow APIs, slow database queries, or sequential requests, the rendered HTML will also be delayed.

A common mistake is writing data fetching code like this:

const user = await getUser()
const bookings =
    await getBookings(user.id)
const recommendations =
    await getRecommendations(user.id)

Sometimes this sequence is necessary. Very often it is not. When data does not depend on the previous result, the page should avoid creating an artificial waterfall.

const [
    user,
    bookings,
    recommendations
] = await Promise.all([
  getUser(),
  getBookings(),
  getRecommendations(),
])

This kind of optimization looks simple, but in real production systems it can remove hundreds of milliseconds from a request. The larger the application becomes, the more important it is to understand which calls are truly dependent and which calls can happen in parallel.

Loading Spinners Can Hide Architecture Problems

Loading states are useful. They help users understand that something is happening. But they can also hide deeper architectural issues.

If every important part of the page appears after a client-side fetch, the user is not really receiving a fast page. They are receiving a shell. The browser loads the shell, then downloads JavaScript, then executes JavaScript, then starts API requests, then waits for data, and only then renders meaningful content.

That pattern may be acceptable for dashboards and private tools. It is usually not ideal for public marketing pages, product pages, landing pages, blog articles, category pages, or SEO-sensitive content.

In many slow Next.js applications, the question should not be “how do we make the spinner nicer?” The better question is “why is this content not available earlier?”

Hydration Problems Grow Slowly

Hydration issues often appear after a project becomes more dynamic. Dates are formatted differently on the server and client. Random values are generated during render. Components read from local storage. Viewport-specific logic is executed too early. Cookie-based personalization changes the rendered output.

const value = Math.random()
const now = new Date()
const width = window.innerWidth

These examples look harmless, but they can create inconsistent output between server and client rendering. Even when they do not break the page completely, they can increase complexity and make performance debugging harder.

The solution is not to avoid dynamic behavior. The solution is to be precise about where dynamic behavior belongs. Server-rendered content should be deterministic. Browser-only values should be read after the component mounts. Personalization should be designed intentionally instead of being mixed into every layout and component.

Third-Party Scripts Are Often More Expensive Than Expected

Many teams carefully optimize their React components and then add five third-party scripts without measuring the result. Analytics, advertising pixels, heatmaps, chat widgets, A/B testing tools, consent managers, and tracking libraries can all compete for the browser main thread.

The biggest cost is not always the download size. The bigger issue is execution. Scripts can block the main thread, delay interaction, trigger layout shifts, and make the page feel unstable.

A production-grade Next.js application should treat third-party scripts as part of the performance budget. They should be loaded intentionally, delayed when possible, and removed when they no longer create business value.

The Backend Still Matters

Next.js performance is not only frontend performance. A page can have a perfect component structure and still be slow because the backend is slow.

In real applications, page speed often depends on database indexes, API aggregation, cache strategy, queue processing, external services, and network latency. If a server component waits for an API endpoint that performs an inefficient database query, the user experiences that database problem as a Next.js performance problem.

This is why serious optimization work should include full request tracing. It is not enough to know that a page took two seconds. The team needs to know where those two seconds went.

Page render:        120 ms
API request:        980 ms
Database query:     720 ms
External service:   260 ms
Hydration:          430 ms
Third-party JS:     350 ms

Without this breakdown, teams often optimize the wrong layer. They reduce a React component by 20 milliseconds while ignoring a database query that costs 700 milliseconds.

Caching Is Powerful, but Only When It Is Designed

Next.js gives teams several caching options, but caching becomes dangerous when nobody understands what is cached, where it is cached, and how it is invalidated.

A mature caching strategy usually has several layers. The browser may cache static assets. A CDN may cache public pages. Next.js may cache fetch results or rendered routes. The backend may cache expensive computations. The database may benefit from query cache behavior, indexes, and warmed pages.

The problem starts when teams add caching as a quick fix. A slow page becomes fast until the data is wrong. Then cache invalidation becomes urgent. Then developers add manual revalidation. Then different environments behave differently. Eventually, nobody is fully confident which version of the page the user is seeing.

Good caching starts with classification. Some pages can be cached aggressively. Some pages can be stale for a short time. Some pages are user-specific and should not be shared. Some data can be cached, while the surrounding page must remain dynamic.

Static Generation Is Not a Magic Solution

Static generation and incremental regeneration can be excellent for content-heavy pages. Blog articles, documentation, landing pages, and many catalog pages can benefit from being generated ahead of time or reused across requests.

But static generation becomes complicated when the page contains frequently changing prices, availability, user-specific content, permissions, experiments, or location-based variations.

The mistake is not using static generation. The mistake is pretending that every page fits the same model. A high-performance application often uses multiple rendering strategies. Static where possible, dynamic where necessary, streamed where useful, and client-side only where interactivity truly requires it.

Middleware Can Become a Hidden Performance Layer

Middleware is useful for redirects, authentication checks, experiments, localization, and request handling. But when too much logic is placed in middleware, every request starts paying for it.

Over time, middleware can become a hidden application inside the application. It checks cookies, reads headers, applies geo rules, handles redirects, assigns experiments, and decides whether a user can access a page.

This logic should be kept small, measurable, and predictable. If every request passes through complex branching before it even reaches the page, the application becomes harder to debug and easier to slow down.

Global Providers Can Make Every Page More Expensive

Another common source of performance decay is the uncontrolled growth of global providers. Authentication providers, theme providers, analytics providers, modal providers, notification providers, feature flag providers, and state management providers are often added at the root of the application.

This feels convenient because every component can access everything. But convenience has a cost. If a provider is placed too high in the tree, it may affect pages that do not need it. It may increase client JavaScript, hydration work, and re-render complexity across the entire application.

Providers should be placed as close as possible to the parts of the application that actually need them. A blog article does not need the same client-side runtime as a logged-in dashboard. A public landing page does not need every booking-flow provider.

Images Can Quietly Become a Bottleneck

Images are often one of the largest parts of a page. Next.js image optimization helps, but it does not replace good image strategy.

A page can still become slow if it loads too many images, uses images with incorrect dimensions, renders hidden galleries, prioritizes the wrong image, or generates too many variants. Image optimization should be combined with product decisions about what really needs to be loaded immediately.

The most important image on the page should be loaded intentionally. Secondary images should not compete with it. Galleries, sliders, and below-the-fold images should be designed with loading behavior in mind.

Performance Problems Are Usually Cross-Layer Problems

The hardest Next.js performance problems are rarely isolated to one component. They usually cross several layers.

A product page may be slow because the React tree is too client-heavy, but also because the API returns too much data, and also because the database query is missing an index, and also because the page loads too many third-party scripts. Fixing only one of those issues may not be enough.

This is why performance work should be measured from the full user journey. The team should understand server response time, API latency, database cost, JavaScript bundle size, hydration time, layout shifts, and interaction delay.

How to Prevent Next.js Performance Decay

Preventing performance decay is easier than fixing it after the system has already become slow. The most effective teams introduce simple rules early and keep them visible during development.

Client components should be intentional. Data fetching should be reviewed for waterfalls. Backend calls should be measured. Expensive queries should be profiled. Third-party scripts should have owners. Caching rules should be documented. Large dependencies should be questioned before they become permanent.

Performance should also be part of code review. A pull request that adds a new provider, a new script, a new API call, or a new client-side dependency should be reviewed not only for correctness, but also for runtime cost.

A Practical Review Checklist

When reviewing a Next.js page that feels slow, start with the full request path. Do not assume the frontend is the problem and do not assume the backend is the problem. Measure both.

Check whether the page is static, dynamic, or unnecessarily dynamic. Check whether data requests are sequential without reason. Check how much JavaScript is shipped to the browser. Check whether client components are used only where interactivity is needed. Check whether third-party scripts are delaying interaction. Check whether backend endpoints perform unnecessary work. Check whether the database uses the right indexes.

The goal is not to make every page static or remove every client component. The goal is to make every expensive decision visible.

What a Healthy Next.js Architecture Looks Like

A healthy production Next.js architecture has clear boundaries. Static content is static. Interactive parts are isolated. Expensive backend operations are measured. Caching rules are explicit. Third-party scripts are controlled. Data fetching is designed, not scattered.

The best systems are not fast by accident. They are fast because teams repeatedly ask the same questions: does this need to run on the client? Does this data need to be fetched now? Can this request be cached? Is this query indexed? Is this script worth its cost? Can this work happen later?

This discipline becomes more important as the system grows. A small project can survive many inefficient decisions. A large production system cannot.

Conclusion

Next.js projects become slow over time because applications grow in complexity. More client JavaScript, more data fetching, more backend dependencies, more scripts, more personalization, and more caching rules all increase the cost of delivering a page.

The solution is not to abandon Next.js. The solution is to use it with production discipline. Keep server and client responsibilities clear. Measure the full request path. Treat caching as architecture. Profile backend calls. Reduce unnecessary JavaScript. Review performance before it becomes an emergency.

Fast applications are not only built with good frameworks. They are built by teams that understand where time is spent.

Make Your Next.js Application Faster

RankSoft helps companies profile slow web applications, optimize frontend and backend bottlenecks, improve caching strategies, and make production systems faster without unnecessary rewrites.

Schedule a Meeting →