Migrating from React to Next.js
Next | January 02, 2024
How I migrated a live production service to a completely new environment.
The choices I made during the migration and where they led.The Problems Our Product Faced
Wanted Insight, acquired by Wanted in 2017, was a project that had sat largely unmaintained until 2022. When the team decided to rebrand Wanted Insight, validating product-market fit as quickly as possible became the top priority — and I recognized the need to redesign the architecture to keep up with the rapid pace of change.
I identified the following core problems:
-
Fragmented code due to poor conventions: Since the product heavily relied on country-specific data, there was a massive amount of data-processing code scattered throughout the codebase. It became increasingly difficult for developers to reason about the code when modifying or building new features.
Given how fast the team needed to ship, this hit the hardest.
-
SEO needs that a custom Express setup couldn't address: I needed SSR (Server-Side Rendering) for better SEO, but the custom Express server wasn't up to the task — and trying to force it would only create more legacy debt.
-
Slow builds and sluggish performance from excessive library usage: Too many unnecessary libraries had bloated build times and degraded runtime performance.
I'll walk you through the approach I chose to tackle these problems and the results I achieved.
Deciding How to Solve the Problem
I narrowed it down to two main approaches:
Starting from Scratch
In some ways, this is the easiest — and most fun — option. It's the simplest way to address the SSR needs and establish proper code conventions. However, as mentioned, the service was already live and shipping new features every sprint. If I went this route and it didn't work out, I'd just be piling up more legacy code — making this the highest-risk option.
Improving the Existing Project
This is the most realistic approach, but also the hardest. It requires deep understanding of the existing codebase, a clear refactoring roadmap — and the discipline to keep shipping features at the same time.
Surprisingly, I ended up using both approaches. (I'll explain what I mean by that below.)
Solving the Problem
-
Designing the Architecture Before starting the refactor, I ran a team study to design a structure that best fit how the team works.
As a result, I adopted the following architecture for Wanted Insight: How We View Components and a Future-Oriented Frontend Architecture. I identified state management as the most problematic area in the codebase. I established new rules for state management and applied them to all newly written code. (For more details on state management, see The Journey to Zustand: Simplifying a Complex Redux Structure.)
-
Removing Unnecessary Libraries and Converting to TypeScript The project was originally built in JavaScript, and I wanted to transition to TypeScript, the standard across Wanted. While converting everything at once would have been ideal, I took an incremental approach — prioritizing modules and refactoring them in order.
For new feature development and feature modifications, I wrote the relevant files in TypeScript. Shared components and utility functions were given higher priority for TypeScript conversion. I cataloged everything that was left and worked through it sprint by sprint.
I also felt there were too many unnecessary libraries in the mix. I set criteria and removed libraries that met them:
- UI libraries
- Lodash
- Date libraries
The biggest challenge during refactoring was testing. In most cases, I had to write custom implementations to replace existing libraries, which risked introducing bugs into the live service. To mitigate this, I wrote test cases for all utility functions after converting them to TypeScript, which gave me at least some peace of mind.
For UI library replacements, instead of writing test code, I added them to the test map and ran QA sessions with the QA team. (Seriously — huge shoutout to the QA team for bearing with me on that.)
- The Big Switch to Next.js and Sharing the Results
Once the TypeScript conversion rate was high enough and a significant portion of the system had been migrated through steps 1 and 2, I prepared for the transition to Next.js.
At Wanted, after a major sprint ends, the makers — developers, designers, and QA — can request a two-week "grooming sprint" to revisit missed items and address technical debt. I decided to use this grooming sprint for the migration. I transferred the restructured folder layout, TypeScript code, and utility functions to the new Next.js project, ran thorough internal testing, and established the deployment process before going live. Since critical bugs during deployment could negatively impact the service, I partnered with the DevOps team to run a canary deployment, gradually shifting traffic between the old and new codebases while monitoring for issues. While I can't share every consideration and discussion, through these steps, Wanted Insight successfully completed the migration to Next.js.
After the transition, I wrapped up by sharing the results and metrics with the squad and the frontend chapter.
| Wanted-Insight Legacy | Wanted-Insight for Next | |
|---|---|---|
| Bundle Size | 5.21MB | 700KB |
| LCP | 1.5s ~ 1.7s | 0.6 ~ 0.8s |
| SEO Impressions | 0 | 230K/month |
As the numbers show, cleaning up the legacy code paid off in a big way. By removing the excessive UI libraries and leveraging SSR for initial data rendering, I also improved LCP performance. Most importantly, the SEO impressions I had prioritized reached 230,000 per month within just six months. If the opportunity arises, I'd love to share more about the SEO improvement process and results.
Closing Thoughts
The migration to Next.js meant far more than simply changing the project's tech stack. I designed new conventions, moved to type-safe code, planned the future of the service, and essentially swapped out the engine so the product could hit its targets. Throughout this process, I communicated with many different departments and received tremendous support. I learned that pulling off something this big is only possible when everyone works as one team.
Every team's situation is different, so the amount of legacy each team can tolerate varies. That's why it's important to constantly reflect on whether your technical debt has grown beyond what your team can manage.
This experience was truly unique — one I don't think I'll ever go through again. But if you find yourself facing a similar challenge, I hope my story helps.