The old setup
Our site had been running on the Pages Router since it was first built. It worked fine — but every new feature felt like it was fighting the framework instead of working with it. Data fetching was scattered across getServerSideProps calls, and layouts had to be manually re-composed on every page.
When the App Router hit stability, we decided it was time.
What we were hoping to gain
- Server Components by default — less client-side JavaScript shipped to the browser
- Nested layouts — shared UI (navbars, sidebars) without prop-drilling or duplication
- Colocated data fetching — no more prop chains five components deep
The migration, honestly
It took about three weeks for a mid-sized site. Here's what actually happened.
Week 1: The easy wins
Static marketing pages moved over almost for free. Server Components meant we could delete a surprising amount of useEffect + useState boilerplate that existed purely to fetch data on mount.
// Before: client-side fetch just to render static-ish content
useEffect(() => {
fetch('/api/posts').then(res => res.json()).then(setPosts)
}, [])
// After: just... fetch it. It's a Server Component.
const posts = await getPosts()
Week 2: The friction
Not everything was smooth.
- Any component using
useState,useEffect, or browser APIs needed'use client'— easy to forget, and the errors weren't always obvious - Third-party libraries that assumed a browser environment needed wrapping in dynamic imports with
ssr: false - Context providers had to move to the boundary between Server and Client Components, which meant restructuring a few layout files
"The mental model shift matters more than the syntax. Once 'this runs on the server unless I say otherwise' clicks, everything else follows."
Week 3: Performance tuning
| Metric | Before | After |
|---|---|---|
| First Contentful Paint | 1.8s | 1.1s |
| JS shipped to client | 340kb | 210kb |
| Time to Interactive | 2.6s | 1.7s |
The client-side JS reduction came almost entirely from moving data-fetching logic and static content out of Client Components.
What we'd tell past us
- Migrate route by route, not all at once — running both routers side by side is fully supported and saved us from a big-bang rewrite
- Audit every
'use client'boundary before moving on — it's easy to mark a whole tree client-side "just to be safe," which quietly kills the benefit - Budget real time for third-party library compatibility — this was the single biggest source of delays
Was it worth it?
Yes — but not because of any single dramatic win. It was the accumulation of smaller things: faster pages, simpler data fetching, fewer files fighting each other for state. If you're on the fence, migrating incrementally removes almost all the risk.
Tech Valley Corp
Nikunj Kumar, CTO & COO





