We’ve worked on the NextJS POC, to evaluate if we can use this tool for our future development. The assumption was NextJS can help us build a scalable front end in terms of performance. And also figure out what kind of infra, workflow changes we will have to do to accommodate this.
Since NextJS fundamentally works differently, this also means that the migration cost is not low. But most of this cost is related to a lot of unknowns in this flow like:
- Can pages be incrementally migrated to next instead of one major refactor?
- Since mobile and desktop pages use the same URLs, can they incrementally migrated?
- What will be the cost of migration? Can this be reduced?
- What infra will we need to achieve this?
- How do we do this without increasing technical debt and can we use this migration to reduce debt?
- How much boost in performance can we expect?
- Can we re-use any old work we’ve already done?
- What are the pros and cons of this migration?
- How will this affect developers and designers with their workflow going forward?
- Finally, the most important question being, is migration worth it?
To figure out all these unknowns it makes sense to create a quick and dirty POC, instead of going through the whole effort of migration only to later figure out later it was a waste of time. The objective of this POC was simple, to figure out these unknowns with as little future cost as possible.
Since this POC didn’t need to go production, this helped us to move fast and because we do not worry about the perfect solution or correctness. This POC completely ignored the feature parity that we believe will not have any impact on performance. We wanted to replicate how the final product will look performance-wise as close as possible. We also wanted to figure out what will be the best approach to infra and implementation for these migrations going forward.
We have decided to do POC for the profile page because it has a fairly complex implementation in the current app and experience is also distinct from other pages. It would paint a good picture of how the actual implementation might behave.
We also didn’t want to spend any time rewriting any old implementation. This way we can be sure the how our old implementation will behave with the next as it is. Also to know for sure improvement is not just because of rewrite.
Can pages be incrementally migrated to next instead of one major refactor?
Yes, this is feasible. NextJS comes with proxy level abstraction, which will let us migrate to NextJS incrementally without changing any current URL structure.
We can have the profile page render with next, while my matches page is rendered by the current app without much abstraction cost.
However, there is a catch, that when picking up pages for migration. It is better to pick up pages that already have a discrete user experience with the rest of the pages. This will make the switch between two infra smoother, with less jank for the user.
For example payment page or user registration page have a distinct user experience they don’t share any layout with the rest of the pages. So when a user switches to these pages the user experience is not janky.
Since mobile and desktop pages use the same URLs, can they be migrated separately?
Yes, the same proxy abstraction mentioned above can also be used to achieve this.
What will be the cost of migration? Can this be reduced?
There are two directions we can take here.
1. For pages that already have technical debt, it makes sense to rewrite the page with a clean slate and new specs. Bad implementation with technical debt will not be performant in any framework. Most of the infra unknowns are figured out, so that should not add much to the cost. We may need a couple of iterations to ease out these quirks though. The cost of migrating pages with fewer moving parts like payment or registration should be more or less the same as building a fresh any non-next js implementation.
2. For newer pages that don’t have much technical debt. The cost of migration is proportional to the complexity of separating the concerns between client and server. Higher the coupling of a page with the client, the higher the cost. This is more like a refactor than a rewrite. Should not typically take more than a couple of sprints, provided code already has less coupling with the client. For example, the new registration flow we worked on a year back should have less debt should be fairly easy to refactor using new infra.
The cost of these migrations can be significantly reduced if there is a single source of truth for product specs, living documentation in tools like confluence, or Github wiki. This will help us by not having to migrate functionality that is never necessary in the first place.
How do we do this without increasing technical debt and can we use this opportunity to reduce debt?
Maintaining two infra is clearly costlier than maintaining one. There is no workaround there. This will also mean similar code can be duplicated for both infra. The new mono-repo infra can make the code duplication and context switch between both infra painless and also make CRA code more modular.
What infra will we need to achieve this?
One infra necessity was mono-repo to facilitate code reuse, deploys, and reduce context switch costs of development. We need this infra regardless of the next POC to make the existing code modular. So we went ahead and created a production-ready version of this instead of POC.
Another main infra necessary is the DevOps pipeline. Since NextJS needs server runtime, this needs to be run ECS containers. So we implemented a deploy pipeline using our GitLab pipelines and mirror repositories. However, this still has some quirks to be ironed out like domain redirections, Datadog monitors, etc.
Can we re-use any old work we’ve already done?
Yes, the mono-repo can help us reuse parts of old code. Provided the code is loosely coupled with the client. A good example is reusing the API abstraction layer by extracting it to a different package.
How much boost in performance can we expect?
The best reason to use Nextjs is to keep scripting costs constant across device types, no matter if the device is higher-end or lower-end. And to reduce time to first meaningful paint times across all device types.
For example, the initial render time will be constant for any kind of device since network and device specs will have less to no effect on this. To put things into context, this is what we’ve noticed in our lighthouse performance reports.
TTI : 12.5
Peformance score: 39
TTI : 7.6
These numbers are in POC with pretty much the same old implementation with next. Note that FCP in the case of next is more meaningful than the old app because in the context of the old app FCP is time to show loading while in the next’s context is actual consumable content like text.
LCP took 5.5s while my Shaadi is 13.5s. This 5.5s is less dependent on device or network conditions.
However, expect to have ~20% deviation with full feature parity and no cleanup. Full feature parity with cleanup may not have this deviation. Also note that these only synthetic tests, real-world results will wary.
Like any framework initially, this will take time to mature and give better results. It’s good to not look at next as a silver bullet to all our performance problems. Performance improvements are ongoing workflows much like any other product workflow. With a focus on lowering experimentation costs, we can reach our ideal state faster.
It is important to look at NextJS as a tool to have faster first meaningful paints, rather than a framework. There is no such thing as a full server-rendered site. Modals, animations and user engagement always happen to the client. NextJS can only reduce initial render costs, the rest is up to the implementation details.
Another thing to note is next apps typically are not progressive in nature since they don’t have intermediary states like loading screens. So progressive web apps and next apps don’t really go hand in hand. However, programmatically apps with no intermediary states also have lower complexity.
What are the pros and cons of this migration?
- Consistent reliable performance across different device types.
- Low latency server to server round trips to API servers.
- Faster first contentful paints.
- Can be used as a BFF proxy server to reduce client payload further. We can offload lot of work to server, keep client payload ligher.
- Differential loading.
- Nextjs Ecosystem.
- No caching,preload, localStorage, history, window size, or any other browser-related APIs for initial renders.
- Lesser progressive behavior.
- Too opinionated sometimes, not working with browser api can become restrictive.
How will this affect developers and designers with their workflow?
- Since next doesn’t know anything about the device width (Although there are workarounds for this problem, these are fragile). We save ourselves lot headache by not introducing this accidental complexity and create responsive designs instead. Meaning any design that takes window width or height into consideration has to go away.
- Separate client and server concerns. server’s concern should be to have faster first paint and reduce client payload, all other concerns can be offloaded to the client.
- Write UI with no intermediary states like loading or window events. This can a happy problem because it can reduce your complexity. Parallelize more work with workers.
- Forget thinking in terms of SPAs. Have page-related concerns well separated.
Finally, the most important question being is migration worth it?
While the next migration can look like a huge leap, this leap is really optical in nature. The cost of building NextJs and the CRA app is pretty much the same. Most of the patterns we use currently can also be re-used for the next app.
- Directionally yes. BFF and faster first paints are worth the effort. They’ll make app very responsive on low end devices.
- New next pages have similar cost to building any spa page.
- Expect few iterations to reach the ideal state.
- Good chance to clear old debt and build product with a lower cost of change.
Big shout out to Pratik, Hussain and Shubham for helping with this POC.