A large-scale Gatsby-to-Next.js migration
basement.studio recently helped a client migrate a content-heavy site from Gatsby to Next.js. The project involved a codebase with more than 30 static pages, around 600 dynamic pages, and 15 language translations for every one of those pages. That scale came with a familiar set of problems: an accumulation of plugins, various escape hatches, and build times that had become genuinely painful.
The motivation wasn't just developer comfort. The redesign presented an opportunity to improve developer experience (DX), cut build times, and adopt more versatile APIs, all of which would enable faster iteration and ultimately a better experience for end users.
Why Vercel made the switch low-risk
The client was already hosting on Vercel, which removed a major category of risk. The team only needed to focus on the code migration, not infrastructure configuration. Instant rollbacks to the last known good version were possible if something went wrong. That safety net was supplemented by several Vercel features that made the migration smoother:
- Preview Deployments let the team test each migrated page individually, with build times around five minutes during QA.
- Incremental adoption was straightforward, since the site remained on Vercel throughout. Pages could be migrated piece by piece without an infrastructure migration hanging over the project.
- Headless hosting meant the existing headless CMS, Ghost, continued to work as-is. The team only had to account for the CMS's APIs while Vercel handled the rest.
- Performance tracking via Vercel Analytics and Checks helped confirm that the migration only improved things. The Real Experience Score rose from 79 to 90 points over the course of the project.
Extensibility: primitives vs. plugins
The core architectural difference between the two frameworks came down to extensibility. Next.js provides primitives that can be adapted to different use cases, while Gatsby tends to offer a plugin for nearly every problem. That means a new feature in a Gatsby site often hinges on finding and installing the right plugin, which can be easy to start with but harder to customize later.
That philosophical difference had real consequences across the codebase.
Image optimization
Image optimization is a good illustration. In Gatsby, the team needed gatsby-plugin-image, plus gatsby-plugin-sharp, and additional packages depending on whether images were static (gatsby-source-filesystem) or dynamic (gatsby-transformer-sharp). In Next.js, the equivalent was a single import:
import Image from 'next/image'
The practical difference was significant: 31 lines of code plus three npm packages in Gatsby versus seven lines in Next.js. The Next.js Image component also accepts props to control loading behavior and layout declaratively, which made the codebase easier to reason about and less vulnerable to future version compatibility issues.
During the migration, image work turned out to be mostly deletion of code and watching for style regressions.
Remote data fetching
This project pulled data from several external sources: Ghost for blog posts, YouTube for videos, and GitHub for repository information. In Gatsby, each source needed its own plugin: gatsby-source-ghost, gatsby-plugin-ghost-images, gatsby-source-youtube-v3, and gatsby-source-custom-api. Each plugin had its own documentation and was consumed differently across the app. In Next.js, the team simply used fetch and passed data down via getStaticProps.
That change brought data flow into plain sight. In one real example from the client codebase, understanding how the stargazers_count reached the UI required tracing a path through an npm package, gatsby-config.js, gatsby-node.js, and the render function. In Next.js, it all lived in one file with two functions. Colocation was a significant win; developers no longer had to deal with separate Gatsby configuration files.
Gatsby also routes all data through GraphQL, which adds friction when the remote APIs don't expose a GraphQL endpoint. In Next.js, the team could use GraphQL, plain fetch, or Node's fs as needed.
Taming build times with ISR
The client's Gatsby builds were hovering around 35 minutes. With roughly 10,000 static pages generated per build (the product of static and dynamic pages across all locales), every push meant a long wait before progress could be shared. Slow iteration cycles made shipping new features a drag.
Next.js offered a way out through a hybrid rendering strategy. The team generated only the most important pages at build time and left less frequently visited and localized pages for Incremental Static Regeneration (ISR). With ISR, when a user requests a page that isn't cached, the server generates it on the fly, returns the HTML, CSS, and JS, then pushes the page to the content delivery network (CDN). The first visitor takes a hit from hitting the origin server, but everyone after that gets a fast CDN response.
That tradeoff was acceptable, and the results were clear: build times dropped from 35 minutes to five.
The outcome
A small team completed the migration in three weeks. The site experienced zero downtime, and internal users kept working with their familiar headless tools throughout. The final numbers were a strong argument for the move: an 85% reduction in build times, a 14% improvement in Real Experience Score, and a steep reduction in the total lines of code.



