Migrating Keeps.com to Next.js

Lin Kelvin
Thirty Madison Engineering
5 min readJan 14, 2022

Keeps is the longest-running brand at Thirty Madison, and with that comes a number of unique challenges. One of these challenges is all too familiar to many developers: the need to maintain a legacy codebase while still developing new features.

Back when Keeps was the only brand under Thirty Madison’s belt, the site was built as a Rails app. As the business requirements grew, there was a desire to move our code away from the legacy rails app and into a more modern frontend framework. That spawned the current frontend app — a react-router app built to handle our logged-in experience.

The existence of these two apps creates a situation where we now need to not only maintain the legacy rails app but also develop new features on the react-router app. As the gap between coding styles and feature parity grew between the two apps, it was becoming a hassle to maintain both codebases simultaneously.

The Impetus for Change

Aside from being annoying to developers, there wasn’t an immediate business reason to fully transition all of the legacy code to the new react-router app. It would take a significant amount of engineering hours, which ultimately would have impacted the development of new features. As such, we simply minimized any development necessary on the legacy app and focused all of our efforts on the new codebase.

This was until we decided to redesign the Keeps conversion funnel.

The new conversion funnel redesign was meant to not only move one of the core user experiences of our site to the new codebase but also to allow us to iterate more quickly on designs. While the initial launch of the new funnel went smoothly, we quickly realized that the performance of the new conversion funnel was actually hurting our conversion rate.

Why Next.js

The decision to move to Next.js was driven by a combination of technical, project management, and marketing features:

Server-side rendering

  • Fully rendered HTML from the server is more friendly to web crawlers, resulting in better SEO
  • Faster initial paint

Code Splitting

Rewrites

  • Can allow for piecemeal migration of one feature at a time
  • Allows for more seamless user experience

Challenges

Migrating to Next.js from our existing react-router app was pretty straightforward. Many components could be copied over as-is with only minor refactoring. However, this didn’t mean that the entire process was smooth sailing.

Data fetching

The biggest challenge we faced was refactoring our pages to fetch data server side. Our original react-router app relied mostly on client-side Apollo hooks. This contributed to an issue where we were unable to display any content on the frontend while the browser was still fetching data for the current page.

One convenience we implemented was heavily based on the example provided by Vercel for integrating Apollo with Next.js. This implementation of the Apollo client passes the Apollo state as it exists server-side to the client. This setup allows Apollo to maintain its cache on the client, reducing the number of requests made using useQuery hooks.

Once the Apollo client was set up, all that was left was to scour the existing codebase for any requests that happen client-side. Once we had an understanding of what kind of data was needed for each page, we wrote a query that would fetch all of the data for that page server-side. Additionally, we decided to create fragments for each model that exists in our database (user, product, etc). This helps ensure two things: 1) type consistency whenever we reference a model that we’re fetching from GraphQL. 2) improve code readability.

Query params and local storage (product ids)

One issue we did run into with server-side rendering was figuring out how to handle query params and local storage. For context, on Keeps we store the most recently selected products for any given user in Local Storage. Storing products like this allows us to create an experience where a user who leaves the site and then returns will have their cart saved, even if they haven’t yet created an account with us. However, we also have an override for this functionality set up where if query params are provided, we want to override whatever the user has currently selected with what is provided via the query string.

Resolving this interaction was a bit complicated on the server side. Do we prefetch product data based only on the query string? Do we prefetch all product data? The way we decided to resolve this issue was by leveraging the fact that we pass the Apollo cache from the server to the client. When a user has no query string, we simply fetch and cache all products that are relevant to the page they are on, and resolve Local Storage on the client side.

Results

Once we had the migration for our conversion funnel pages complete, we needed to measure and compare the performance of our Next.js implementation compared to the existing app. Performance would be measured on two axes:

  • Core web vitals
  • Conversion rate

Let’s start with the more obvious of the two choices. We started this migration to Next.js as an attempt to improve app performance, and so starting with the core web vitals is a good place to start. To measure these, we utilized Datadog’s RUM feature. Adding RUM to both our existing app as well as the Next.js app allows us to create a dashboard directly comparing the web vitals between the two apps.

What we found was that the core web vitals were better across the board in Next.js. Next.js’s automatic code-splitting for each page, as well as the prefetch feature, really helped us reduce load times across the funnel (about a 30% improvement in time-to-interactive, and 70% improvement in first input delay).

Our data and marketing teams owned measuring the impact on our conversion rate. We captured the data through Segment as well as Launch Darkly and calculated which percentage of users actually made a purchase. After our test was complete, we found that the conversion rate between the two apps was not significantly different.

While that may seem like we did a lot of work for nothing, moving to Next.js still gives us a better foundation as an engineering team by allowing us to slowly remove code that isn’t used. Moving away from the legacy codebase will help improve overall sprint velocity, and the improvement in core vitals will also help with SEO in the long term.

Future Work

The effort to transition the rest of our application to Next is still in progress. We have many more features and pages in our app that we’re looking to move over, and the future looks bright given the results of these initial migrations.

--

--