DEV Community

Cover image for Your Next.js Website Feels Slow? Check These 7 Things Before Blaming the Server
Anas Sheikh
Anas Sheikh

Posted on

Your Next.js Website Feels Slow? Check These 7 Things Before Blaming the Server

Your Next.js application might not be slow because your server is slow.

It might be slow because you accidentally made the browser do work it never needed to do.

I've seen developers increase server resources, change hosting providers, add caching, install optimization packages, and rewrite API calls...

...only to discover the real problem was a component importing a huge library for one tiny feature.

So before you blame Vercel, your VPS, MongoDB, your API, or your internet connection, check these seven things.

Some of them take less than five minutes to find.

1. You Imported an Entire Library for One Function

This is one of the easiest performance problems to create.

Imagine you only need one utility from a package:

import _ from "lodash";
Enter fullscreen mode Exit fullscreen mode

and later:

const sortedUsers = _.sortBy(users, "name");
Enter fullscreen mode Exit fullscreen mode

You might not notice anything during development.

But now you're potentially bringing much more code into your application than you actually need.

Instead, prefer targeted imports when the package supports them:

import sortBy from "lodash/sortBy";
Enter fullscreen mode Exit fullscreen mode

Even better, before adding a dependency, ask:

"Can I write this myself in five lines?"

Sometimes the answer is yes.

Don't install a 70 KB library to avoid writing:

const sorted = [...users].sort((a, b) =>
  a.name.localeCompare(b.name)
);
Enter fullscreen mode Exit fullscreen mode

Dependencies aren't free.

Every package you add becomes part of the complexity your application has to manage.

2. You Turned a Tiny Component Into a Client Component

This one is especially important with the App Router.

You write:

"use client";
Enter fullscreen mode Exit fullscreen mode

at the top of a component because you need one small interaction.

Maybe it's just a dropdown.

Maybe it's a button.

Maybe it's a small animation.

But now you've potentially moved more of your component tree into the client-side world than necessary.

For example:

"use client";

export default function Dashboard() {
  // 500 lines of UI
  // database-related data
  // charts
  // tables
  // static content
  // one dropdown
}
Enter fullscreen mode Exit fullscreen mode

The problem isn't that Client Components are bad.

They aren't.

The problem is using them when you don't actually need them.

A better pattern is to keep the page server-rendered and isolate the interactive part:

Dashboard
├── Header
├── Stats
├── RevenueChart
├── RecentOrders
└── FilterDropdown ← Client Component
Enter fullscreen mode Exit fullscreen mode

Instead of:

Dashboard ← Client Component
├── Header
├── Stats
├── RevenueChart
├── RecentOrders
└── FilterDropdown
Enter fullscreen mode Exit fullscreen mode

Make the interactive island interactive—not the entire ocean.

3. You're Using useEffect to Fetch Data That Could Be Fetched on the Server

I've seen this pattern hundreds of times:

"use client";

useEffect(() => {
  fetch("/api/products")
    .then(res => res.json())
    .then(setProducts);
}, []);
Enter fullscreen mode Exit fullscreen mode

It works.

But ask yourself what happens.

The browser has to:

  1. download JavaScript
  2. execute JavaScript
  3. render the component
  4. make the request
  5. wait for the response
  6. update state
  7. render again

If the data doesn't require browser interaction, you may not need that entire process.

With a Server Component, you can often do:

export default async function Products() {
  const products = await getProducts();

  return (
    <ProductList products={products} />
  );
}
Enter fullscreen mode Exit fullscreen mode

Now the server can retrieve the data before sending the rendered result.

This doesn't mean:

"Never fetch data in Client Components."

Client-side fetching is completely valid when the data is interactive, user-specific after hydration, frequently changing, or needs browser APIs.

The point is:

Don't automatically turn server work into browser work.

4. Your Images Are Absolutely Massive

You can have perfect React code and still ship a terrible website if you're sending 4 MB images to someone's phone.

This:

<img
  src="/hero.jpg"
  alt="Hero"
/>
Enter fullscreen mode Exit fullscreen mode

isn't automatically wrong.

But Next.js provides image optimization tools for a reason.

For many use cases:

import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={700}
/>
Enter fullscreen mode Exit fullscreen mode

gives the framework more information about how the image should be delivered.

But there's another mistake developers make:

They upload a 5000×3000 image because:

"The browser will resize it."

Sure.

But the user may still have to download the giant original.

Resize and compress your source images too.

A 4 MB hero image isn't a badge of high quality.

It's a performance problem.

5. You're Loading Everything Before the User Needs It

Imagine your dashboard contains:

  • charts
  • analytics
  • maps
  • a rich text editor
  • a PDF viewer
  • a huge data table

But the user only sees the dashboard overview first.

Why load every expensive component immediately?

Dynamic imports can help:

import dynamic from "next/dynamic";

const AnalyticsChart = dynamic(
  () => import("./AnalyticsChart")
);
Enter fullscreen mode Exit fullscreen mode

Now you can defer expensive functionality when appropriate.

The same principle applies beyond components.

Ask:

"Does the user need this right now?"

If the answer is no, consider loading it later.

Performance isn't only about making code faster.

Sometimes it's about not running code yet.

6. You Have a Giant Client-Side State

This is another common one.

Developers sometimes put almost everything into global state:

users
products
orders
notifications
filters
modal state
theme
dashboard data
settings
search results
server responses
Enter fullscreen mode Exit fullscreen mode

Then every part of the application subscribes to it.

Eventually you get a situation where changing one tiny value causes a surprisingly large amount of UI to update.

Global state isn't inherently bad.

But not everything needs to be global.

A useful question is:

"Who actually needs this data?"

If one component needs it, keep it local.

If a small subtree needs it, keep it close to that subtree.

If the entire application needs it, then global state makes sense.

State should live as close as possible to where it's actually used.

7. You Never Looked at Your Bundle

This is probably the biggest mistake on the list.

Developers frequently optimize code based on intuition.

They say:

"This page should be fast."

But they don't actually inspect what the browser receives.

That's backwards.

Measure first.

Then optimize.

Look at:

  • JavaScript bundle size
  • large dependencies
  • duplicated packages
  • client components
  • image sizes
  • network requests
  • third-party scripts
  • hydration work

You don't need to guess.

Your browser can tell you what it's doing.

The Performance Debugging Order I Use

When a Next.js page feels slow, I don't immediately start rewriting components.

I go in this order:

1. Network
2. Images
3. JavaScript
4. Client Components
5. Data fetching
6. Third-party scripts
7. Server/database
Enter fullscreen mode Exit fullscreen mode

Why?

Because developers often jump straight to the backend.

But if the browser is downloading 3 MB of JavaScript and 8 MB of images, moving your API from a $10 VPS to a $100 server isn't going to magically fix the experience.

Find the bottleneck first.

A Simple Test You Can Do Right Now

Open your production website.

Then open:

DevTools
→ Network
→ Reload
Enter fullscreen mode Exit fullscreen mode

Look at the total transferred size.

Then switch to:

DevTools
→ Performance
Enter fullscreen mode Exit fullscreen mode

Record a page load.

Watch what happens.

Ask:

What takes the longest?

JavaScript?
Images?
API requests?
Rendering?
Hydration?
Third-party scripts?
Enter fullscreen mode Exit fullscreen mode

That's your starting point.

Not Twitter.

Not a random optimization blog.

Not an AI-generated list of "10 Next.js tricks."

Your actual application.

The Optimization Trap

There's a dangerous stage in every developer's life where optimization becomes a hobby.

You start doing things like:

"I'll memoize this."

"I'll add another cache."

"I'll lazy-load this."

"I'll use another state library."

"I'll rewrite this component."

"I'll add another optimization package."
Enter fullscreen mode Exit fullscreen mode

But nobody measured anything.

That's not optimization.

That's guessing.

A 5% theoretical improvement to something that takes 20 milliseconds doesn't matter if your biggest problem is a 4 MB image.

Performance Is About User Experience, Not Just Lighthouse Scores

A perfect Lighthouse score is nice.

But your users don't care about your Lighthouse screenshot.

They care whether:

  • the page appears quickly
  • buttons respond immediately
  • navigation feels smooth
  • content doesn't jump around
  • images load without destroying the layout
  • forms don't freeze
  • dashboards don't feel heavy

Performance is ultimately about how quickly your application lets someone accomplish something.

That's the metric that matters.

My 10-Minute Next.js Performance Checklist

Before I deploy a large Next.js application, I like to check:

□ Are unnecessary components marked "use client"?

□ Am I fetching static/server data in the browser?

□ Are my images compressed and appropriately sized?

□ Am I importing large libraries unnecessarily?

□ Can expensive components be dynamically loaded?

□ Is global state actually necessary?

□ Are third-party scripts slowing down the page?

□ Have I inspected the production bundle?

□ Have I tested on a slower device?

□ Did I actually measure the bottleneck?
Enter fullscreen mode Exit fullscreen mode

That last question is the most important.

Did I actually measure it?

Because the slowest part of your application is often not the part you expected.


The next time someone tells you:

"Next.js is slow."

Ask them one question:

"What exactly is slow?"

The server?

The database?

The network?

JavaScript execution?

Hydration?

Images?

Rendering?

Until you know the answer, you're not debugging performance.

You're guessing.

And guessing is expensive.


What's the biggest performance mistake you've found in a Next.js project?

Mine is seeing a tiny interactive component turn an otherwise server-rendered page into a massive client-side bundle.

I'm curious what you've seen in the wild.

Drop it in the comments. 👇

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (1)

Collapse
 
crdtcto profile image
Kane Lim

Hello Glad to see you, I am Kane Lim from Hong Kong. I have over 10 years of development experience. I am writing this because your post was interesting.

I strongly agree with the debugging order. One additional layer I would add is correlating browser telemetry with server traces instead of profiling each boundary independently.

For Next.js, I would instrument Web Vitals, hydration duration, bundle composition, route transitions, server timing, database latency, cache hit ratio, and API p95 latency using OpenTelemetry. Then correlate a slow navigation through a trace ID from browser interaction to React rendering, server execution, database queries, and external APIs.

I would also use bundle analysis in CI with size budgets, dependency regression detection, and automated Lighthouse testing on throttled CPU and network profiles. This catches performance regressions before deployment rather than after users report them.

The key insight is that performance optimization should be treated as an observability problem first and a code optimization problem second. Great practical checklist. I enjoyed reading this and would be happy to exchange ideas on advanced Next.js performance profiling.