Next.js Developers Are Shipping Bloated Apps (Mine Too)
Why Next.js's convenience features are trapping developers into performance debt, and the framework changes that could fix it.
By Sean WeldonNext.js Developers Are Shipping Bloated Apps (Mine Too)
I shipped a 400KB JavaScript bundle for a landing page last month. It loaded three different animation libraries, hydrated components that never changed, and pre-fetched routes the user would never visit. The client was happy because it looked smooth. I was embarrassed because I knew better.
This isn't a rare problem. As a next.js developer, I see it constantly: production apps that download megabytes of JavaScript before showing a simple form. Apps that server-render HTML just to throw it away and re-render on the client. Apps that use 'use client' on everything because it's easier than thinking about the boundaries.
Next.js makes it easy to build fast. It also makes it easy to build slow without noticing.
The Framework Gives You Rope
Next.js ships with incredible performance tools out of the box. Image optimization, automatic code splitting, streaming SSR, React Server Components. The problem is that these features work best when you use them correctly, and the framework won't stop you from doing the wrong thing.
Take next/image. It optimizes images automatically, serves modern formats, lazy loads by default. Great. But I've reviewed codebases where developers imported raw PNGs, manually set priority on everything, or disabled lazy loading because "it felt janky." The optimization exists, but no one's forcing you to use it properly.
Or client components. The new App Router defaults to Server Components, which is brilliant for performance. But slap 'use client' at the top of a file and suddenly that entire component tree runs in the browser. I've seen apps where 80% of components are client-side because someone copy-pasted a template without understanding what they were doing.
The framework is opinionated about performance. Developers often aren't.
My Own Bloat
I'm not exempt from this. Here's what I shipped in that 400KB bundle:
- Framer Motion for page transitions that triggered once
- Three.js for a background effect that didn't need to be interactive
- A markdown renderer that could have run at build time
- Font files that weren't subset or optimized
- Lodash instead of individual utility functions
Every decision made sense in isolation. Framer Motion is clean API. Three.js makes WebGL simple. Markdown rendering is a solved problem. But I never stepped back and asked: does this need to run in the browser?
The answer was no. I could have pre-rendered the markdown. The Three.js scene could have been a static video. The page transitions could have been CSS. The fonts could have been subset to the characters I actually used.
I was lazy. The framework let me be lazy. The app worked. It just worked slowly.
Where the Bloat Comes From
Most performance problems in Next.js apps come from three places:
1. Client-Side Everything
Developers mark components as 'use client' to use hooks, then import those components into Server Components, and suddenly the entire page is client-side. The boundary between server and client gets fuzzy. You lose the performance benefits of SSR without realizing it.
I've seen this with custom web development projects where teams migrated from create-react-app to Next.js but kept the same mental model. Everything runs in the browser because that's what they're used to. They're using Next.js as a fancy bundler.
2. Dependency Hell
NPM makes it trivial to install packages. npm install react-awesome-slider and you've added 200KB to your bundle. Do that ten times and you're shipping a megabyte of code before you write a single line yourself.
The worst part? Half those packages have overlapping functionality. You install date-fns and moment and dayjs because different parts of the codebase use different libraries. No one notices until the Lighthouse score is in the red.
3. Ignoring Build Output
Next.js tells you exactly what you're shipping. After every build, it prints bundle sizes, page sizes, whether a page is static or dynamic. Most developers never look at it.
I didn't. I ran npm run build to check for TypeScript errors, saw green checkmarks, and deployed. I never scrolled up to see that my homepage was 800KB of JavaScript. The information was right there. I ignored it.
What Actually Works
The solution isn't to stop using Next.js. The solution is to use it correctly.
Start with Server Components by default. Only mark components as 'use client' when you genuinely need interactivity. Forms, modals, dropdowns. Everything else should run on the server. If you're not sure, leave it as a Server Component. You can always add 'use client' later.
Audit your dependencies. Run npx next-bundle-analyzer and see what's actually in your bundle. You'll find packages you didn't know you were using. Tree-shaking doesn't work if you import the entire library. Import individual functions instead.
Optimize images and fonts aggressively. Use next/image for everything. Subset your fonts. Lazy load below the fold. These are boring, unglamorous optimizations that make a massive difference.
Look at the build output. Actually read what Next.js is telling you. If a page is 500KB, figure out why. If a route is dynamic when it should be static, fix it. The framework gives you all the information. You have to look at it.
For a deeper dive into structuring Next.js apps correctly, check out my post on Build a Next.js App Without Online Booking, which covers practical architectural decisions that affect bundle size.
The Real Problem
The real problem isn't Next.js. It's that modern frameworks have abstracted away performance concerns to the point where developers don't think about them anymore.
We used to worry about every kilobyte. We minified HTML. We combined CSS files. We counted HTTP requests. Now the framework handles all of that, so we assume it's handled well.
But the framework can't read your mind. It doesn't know that your animated hero section could be a static video. It doesn't know that you imported an entire icon library to use three icons. It doesn't know that your client component could be a server component.
You have to know. You have to care.
Ship Faster by Shipping Less
I rewrote that 400KB landing page. Moved animations to CSS. Pre-rendered markdown at build time. Replaced Three.js with a video. Subset fonts. The final bundle was 85KB.
The page loaded faster. Core Web Vitals improved. The client didn't notice the difference because the UX was identical. I noticed because I actually looked at what I was shipping.
Being a next.js developer means more than knowing the API. It means understanding what runs where, what gets bundled, what gets optimized. The framework gives you the tools. You have to use them.
Want a Next.js app that's actually fast? Let's build it right from the start. I help companies ship lean, performant web applications without the bloat. Check out my custom web development services at sean-weldon.com/webdev.