My honest journey through incremental framework upgrades, breaking changes, and lessons learned
Introduction
When I started this morning, my Next.js portfolio was running smoothly on version 14. By afternoon, after upgrading to version 16, I was debugging 404 errors, dealing with deprecated middleware patterns, and questioning every life choice that led me to this moment.
But here's the thing: upgrading Next.js isn't as scary as it seems. It's just a series of small, manageable steps - kind of like learning to walk again, but digitally.
In this article, I'll walk you through my exact upgrade journey from Next.js 14 → 15 → 16, the unexpected hurdles I hit, the features that impressed me, and the breaking changes that caught me off guard.
Part 1: The Incremental Path - Why Skipping Versions Is Risky
Let me be clear: I didn't jump directly from 14 to 16. That would have been chaos.
The safest approach is to upgrade incrementally:
- 14 → 15 (one major version at a time)
- 15 → 16 (then proceed to the next)
Why? Because each version introduces breaking changes, and doing them one step at a time makes it easier to identify which upgrade broke what.
The Upgrade Process
Step 1: Next.js 14 → 15
npm install next@15
This initial upgrade went smoothly for me. The main things that happened:
- React dependencies updated automatically
- Build process remained largely the same
- Most of my existing code continued to work
Key learning: Update React and React-DOM at the same time:
npm install react@latest react-dom@latest
Step 2: Next.js 15 → 16
npm install next@latest
This is where things got interesting.
Part 2: The New Features That Made Me Say "Finally!"
1. Turbopack is Now Default
Next.js 16 ships with Turbopack as the default bundler instead of Webpack. This wasn't my choice - it just happened.
The benefit: My initial build time decreased from 22 seconds to 11 seconds. That's a 50% improvement.
▲ Next.js 16.2.3 (Turbopack)
✓ Compiled successfully in 11.0s
Turbopack is written in Rust and is significantly faster for development builds. For production builds, you might still use the old behavior, but the development experience is noticeably snappier.
2. Enhanced React 19 Support
With Next.js 16, React 19 support is more mature. My dependencies now look like:
{
"react": "^19.2.5",
"react-dom": "^19.2.5"
}
React 19 brings:
- Server Components improvements
- Better error boundaries
- More predictable state management
- The new
use()hook for cleaner async code
3. Improved Image Optimization
The next/image component in version 16 has better automatic format detection and responsive image handling. Since I have a portfolio with lots of project screenshots, this should help with performance.
4. Better TypeScript Integration
During the build process, Next.js 16 automatically reconfigured my tsconfig.json with better defaults:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "react"
}
}
This means better type checking and IDE autocomplete out of the box.
Part 3: The Breaking Changes That Broke Me (And How I Fixed Them)
🚨 Breaking Change #1: Middleware is Deprecated
The Warning Message:
⚠ The "middleware" file convention is deprecated.
Please use "proxy" instead.
This was the big one. My project uses next-intl for internationalization (supporting English and German), and the middleware is critical for routing requests to the correct locale.
The Problem:
Next.js ran a codemod to automatically convert middleware to proxy patterns:
npx @next/codemod@canary middleware-to-proxy .
After running this, my routes started returning 404 errors:
-
/en/certifications→ 404 -
/de/certifications→ 404 - All locale-based routes → 404
The Solution:
I discovered that next-intl library still relies on middleware. The codemod conversion doesn't work well with i18n setups. So I:
- Kept my middleware.ts file intact:
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'de'],
defaultLocale: 'en'
});
export const config = {
matcher: ['/', '/(en|de)/:path*', '/((?!_next|_vercel|.*\\..*).*)']
};
- Updated next-intl to the latest version:
npm install next-intl@latest
- Cleared the build cache and rebuilt:
rm -r .next
npm run build
The Takeaway: While middleware is supposedly "deprecated," it still works perfectly in Next.js 16 and is necessary for i18n routing with next-intl. The library maintainers haven't migrated to the new proxy pattern yet, and that's okay.
🚨 Breaking Change #2: Missing Dependencies
When I added recharts for data visualization, I encountered:
Module not found: Can't resolve 'react-is'
Recharts has a dependency on react-is that wasn't automatically installed as a peer dependency.
The Fix:
npm install react-is
The Lesson: When adding third-party libraries with Next.js 16, double-check their peer dependencies. The ecosystem is still catching up.
Part 4: What I Actually Built (Real-World Example)
To test all these changes, I added a skills chart component using Recharts:
import {
BarChart,
Legend,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Bar,
} from "recharts";
import { techSkills } from "../api/skills";
// Transform skill data into chart format
const data = techSkills.frontend.map((skill) => ({
name: skill.name,
years: parseFloat(skill.years),
}));
const BarChartExample = ({ isAnimationActive = true }) => (
<BarChart
layout="vertical"
style={{
width: "100%",
maxWidth: "700px",
maxHeight: "70vh",
aspectRatio: 1.618,
}}
data={data}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="name" type="category" width={100} />
<Tooltip />
<Legend />
<Bar dataKey="years" fill="#8884d8" isAnimationActive={isAnimationActive} />
</BarChart>
);
export default BarChartExample;
The source data structure (which remained unchanged):
export const techSkills = {
"frontend": [
{ name: "Angular", years: "7" },
{ name: "React", years: "6" },
{ name: "Next.js", years: "3" },
// ... more skills
],
"backend": [
{ name: "Node.js", years: "7.5" },
// ... more skills
],
};
This example shows how Next.js 16 handles client-side components, external libraries, and data transformations seamlessly.
Part 5: A Checklist for Your Next.js Upgrade
Based on my experience, here's what you should do when upgrading:
Pre-Upgrade
- [ ] Commit your current work to git
- [ ] Document your current version:
npm list next - [ ] Check all test files pass
- [ ] Review the official Next.js upgrade guide
During Upgrade
- [ ] Upgrade incrementally (14→15, then 15→16)
- [ ] Run
npm install next@15first - [ ] Then run
npm install next@latestfor 16 - [ ] Update React dependencies:
npm install react@latest react-dom@latest - [ ] Clear build cache:
rm -r .next - [ ] Run
npm run buildto catch compilation errors
Post-Upgrade
- [ ] Test all major routes in development
- [ ] Check console for deprecation warnings
- [ ] Verify third-party libraries are compatible
- [ ] Test with actual users if possible
- [ ] Review performance metrics (build time, bundle size)
- [ ] Update documentation if needed
Part 6: The Performance Impact
Here's what changed for my project:
| Metric | Before (v15) | After (v16) | Change |
|---|---|---|---|
| Dev Build Time | 22.2s | 11.0s | 50% faster |
| Build File Size | ~2.1MB | ~2.0MB | Slightly smaller |
| TypeScript Check | 8.2s | 6.0s | 27% faster |
| Dev Server Start | 4.3s | 2.1s | 51% faster |
That's a significant improvement in developer experience. Faster builds mean faster feedback loops, which means more productive development sessions.
Part 7: Should You Upgrade? (The Honest Answer)
TL;DR: Yes, but do it incrementally.
Reasons to upgrade:
✅ Turbopack performance improvements
✅ Better React 19 support
✅ Improved image optimization
✅ Better TypeScript defaults
✅ Security updates and bug fixes
✅ Future-proofs your project
Reasons to wait:
⚠️ If you're on a tight deadline with a stable project
⚠️ If you use many third-party libraries that aren't updated yet
⚠️ If you have heavy customization (custom webpack config, etc.)
For most projects, the benefits outweigh the risks.
Conclusion: Feeling Like a Child Who Knows Nothing
When I started this upgrade journey, I felt lost. Deprecated features, 404 errors, missing dependencies - it all seemed overwhelming.
But here's what I learned:
Modern framework upgrades aren't as scary as they seem. They're just a series of small, manageable steps.
The ecosystem matters. Libraries like
next-intlstill rely on middleware, and that's fine. Not everything needs to change just because the framework says it's "deprecated."Performance wins are real. A 50% faster build time isn't just a nice-to-have - it genuinely improves your daily workflow.
The documentation is your friend. When things broke, the official Next.js error messages and docs pointed me in the right direction.
Incremental upgrades prevent disasters. Jumping two major versions at once would have been a nightmare. Taking it one step at a time made debugging straightforward.
Next.js 16 isn't revolutionary - it's evolutionary. It's the framework maturing, getting faster, and becoming more reliable. And that's exactly what we need as developers.
So yes, I felt like a child who knows nothing when I started. But after working through the problems, I realized I wasn't lost - I was just learning.
And that's been the most valuable part of this upgrade.
Resources
- Next.js 16 Release Notes
- Next.js Upgrade Guide
- next-intl Documentation
- Turbopack Documentation
- React 19 What's New
This article is based on real upgrade experiences and best practices for Next.js development. Your mileage may vary depending on your specific setup and dependencies.
Top comments (0)