Our e-commerce site (4,200 product pages) had been limping along on Gatsby 5.13.0 since June. Build times climbed from 12 minutes to 38 minutes over eight months. Memory usage spiked to 7.2GB during createPages
, crashing our GitHub Actions runners with exit code 137 twice a week.
The final straw was December 15th, 2024. Sarah from marketing wanted to update one product description. Simple text change in Contentful, should take 2 minutes to deploy. Build crashed at page 2,847 with JavaScript heap out of memory. Tried again - crashed at 3,156. Third attempt at 4:30 PM? Same fucking error.
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
1: 0xa3f3c0 node::Abort() [node]
2: 0x985c85 node::FatalError(char const*, char const*) [node]
That error message became my wallpaper after seeing it 47 times in two weeks. Marketing team left for Christmas break with their change still stuck in the queue. CTO approved migration budget that Friday.
Week-by-Week Migration Reality (What Actually Happened)
Week 1: Next.js Setup and Basic Pages (Estimate: 4 days, Reality: 7 days)
Monday-Tuesday: Set up Next.js 14 with App Router. Figured out the routing structure, since Gatsby's file-based routing doesn't map directly to Next.js dynamic routes. Had to rewrite our slug handling logic entirely.
Our Gatsby structure:
src/pages/
products/{contentfulProduct.slug}.js
categories/{contentfulCategory.slug}.js
blog/{contentfulBlogPost.slug}.js
Next.js structure:
app/
products/[slug]/page.tsx
categories/[slug]/page.tsx
blog/[slug]/page.tsx
Simple, right? Wrong. Gatsby's GraphQL layer automatically handled slug conflicts between different content types. Next.js doesn't. Had to build custom slug resolution logic to handle cases where a product and blog post might have the same slug.
Wednesday-Thursday: Converted basic pages (home, about, contact). These should have been trivial but spent 6 hours debugging why CSS modules weren't working. Turns out our Gatsby setup had been using css-loader
configuration that Next.js handles differently.
Friday: Started converting the product listing page. This is where the real pain began.
Weekend: Spent Saturday debugging why images weren't loading. Gatsby's gatsby-image
component doesn't exist in Next.js. Had to rewrite every image component to use next/image
. The optimization parameters are completely different, and responsive image handling required rewriting our entire media query logic.
Week 2: Data Fetching Hell (Estimate: 5 days, Reality: 9 days + weekend)
The GraphQL Migration Nightmare: Our Gatsby site had 23 different GraphQL queries pulling data from Contentful, Shopify, and our internal PostgreSQL database. Each query leveraged Gatsby's GraphQL layer for automatic relationship resolution. Here's what one looked like:
export const query = graphql`
query ProductQuery($slug: String!) {
contentfulProduct(slug: { eq: $slug }) {
title
description
images {
gatsbyImageData(width: 800)
}
category {
name
slug
}
relatedProducts {
title
slug
price
}
}
shopifyProduct(handle: { eq: $slug }) {
variants {
price
compareAtPrice
available
}
}
}
`
In Next.js, this becomes:
// app/products/[slug]/page.tsx
async function getProduct(slug: string) {
const [contentfulData, shopifyData] = await Promise.all([
fetch(`/api/contentful/products/${slug}`),
fetch(`/api/shopify/products/${slug}`)
]);
return {
contentful: await contentfulData.json(),
shopify: await shopifyData.json()
};
}
Looks cleaner, but now I had to:
- Write API routes for each data source
- Handle caching manually (no more Gatsby's automatic static generation)
- Implement error handling for failed API calls
- Build loading states (Gatsby pre-rendered everything)
Tuesday-Wednesday: Created API routes for all our data sources. Contentful's REST API documentation is trash compared to their GraphQL endpoint. Spent half a day figuring out how to include references and resolve linked entries properly using their Content Management API.
Thursday-Friday: Implemented data fetching in page components. Next.js App Router's caching is confusing as fuck. The same API call would be cached in development but not production, or vice versa. Had to read the docs three times to understand when fetch calls get cached.
Weekend: Sarah tested the new product pages and found that related products weren't loading. Turns out our Gatsby GraphQL query was automatically resolving relationships between content types. In Next.js, I had to write explicit logic to fetch related items. Added another 8 hours to manually build relationship resolution.
Week 3: Plugin and Feature Migration (Estimate: 4 days, Reality: 8 days)
The Plugin Graveyard: Gatsby plugins don't exist in Next.js. Here's what we had to rebuild:
gatsby-plugin-sitemap
→ Custom sitemap generationgatsby-plugin-robots-txt
→ Manual robots.txt in public foldergatsby-plugin-manifest
→ Manual manifest.jsongatsby-source-contentful
→ Custom Contentful API integrationgatsby-source-shopify
→ Custom Shopify API (thank fuck, their plugin was broken anyway)gatsby-plugin-google-analytics
→ Next.js Analytics componentgatsby-plugin-sharp
→next/image
with custom optimization
Most of these took 2-4 hours each to reimplement. The sitemap was particularly annoying because Gatsby auto-generated it from all your pages. In Next.js, you have to manually maintain the list of routes.
Wednesday-Thursday: Migrated our search functionality. We had been using `gatsby-plugin-local-search` to index all content at build time. Next.js doesn't have an equivalent, so I implemented search using Algolia. Setup was easy following the Algolia React InstantSearch guide, but indexing 4,200 products requires API calls during build, which added 3 minutes to our deployment time.
Friday: Fixed SEO meta tags. Gatsby's SEO component doesn't work in Next.js. Had to rewrite all meta tag generation using Next.js metadata API. The new App Router has different metadata handling than Pages Router, and most Stack Overflow answers are for the old system.
Week 4: Testing and Production Deployment
Monday-Tuesday: End-to-end testing. Found 47 broken links, 12 missing images, and 3 API routes that crashed under load. Our test suite had been written for Gatsby's test environment - had to rewrite most tests for Next.js.
Wednesday: Performance testing. Next.js build time: 4 minutes 23 seconds. Gatsby build time before migration: 38 minutes 12 seconds. Memory usage peaked at 1.8GB instead of 7.2GB. Finally, some good news.
Thursday: Deployed to staging. Everything looked fine until Sarah tried to publish a content update. Forgot to implement incremental static regeneration properly. Content changes weren't showing up without full rebuilds.
Friday: Fixed ISR configuration. Deployed to production. Site went live at 3:47 PM. First bug report came in at 4:12 PM - product filtering wasn't working on mobile Safari. Spent the evening debugging iOS-specific JavaScript issues.
What I Wish Someone Had Told Me
The Hidden Costs
CI/CD changes: GitHub Actions workflow had to be completely rewritten. Gatsby used specific plugins for deployment optimization. Next.js has different caching strategies and build output. Added 4 hours of DevOps work nobody budgeted for.
Team training: Our designer had learned Gatsby's component system. Next.js components work differently, especially with Server Components. Sarah needed 3 hours of training to understand why her MDX changes weren't rendering properly.
Third-party integrations: Our CMS webhook system was configured for Gatsby Cloud's build triggers. Vercel's deployment hooks use different endpoints and authentication. Our content team couldn't publish updates for 2 days while we reconfigured everything.
The Migration Tax
Every Gatsby site pays "migration tax" - work that doesn't exist in greenfield Next.js projects:
- Converting GraphQL queries to fetch calls: ~2-3 hours per complex query
- Replacing Gatsby-specific plugins: ~2-4 hours per plugin
- Image optimization migration: ~6-8 hours for media-heavy sites
- Routing and slug handling: ~4-6 hours for complex URL structures
- SEO and metadata: ~3-4 hours to reimplement properly
Our site had 23 GraphQL queries, 12 plugins, 400+ optimized images, and complex category-based routing. Migration tax: 67 hours of pure conversion work that added no new features.
What Actually Broke on Day One
Product variant selection: Our JavaScript assumed Gatsby's build-time data was always available. Next.js lazy-loads some data, breaking our variant switching logic.
Image lazy loading:
gatsby-image
's intersection observer conflicted withnext/image
's built-in lazy loading. Double lazy-loading meant images never loaded on slow connections.Client-side routing: Gatsby preloads page data on link hover. Next.js doesn't do this automatically. Page transitions felt slower until we implemented custom prefetching with the Next.js Link component.
Form submissions: Our contact forms were using Gatsby's built-in form handling. Next.js requires API routes for form processing. Forms silently failed until we rebuilt the submission logic.
Search indexing: Google Search Console showed 80% of pages returning 404 errors. Our URL structure had subtly changed during migration (trailing slashes, query parameters). Spent a weekend implementing Next.js redirects and URL rewriting rules.
The Business Case That Actually Worked
Here's what convinced our CTO to approve 4 weeks for migration:
Cost Analysis (Monthly)
Before (Gatsby):
- GitHub Actions: $340/month (47-minute builds, 3-5 daily)
- Developer time debugging: ~20 hours/month @ $75/hour = $1,500
- Failed deployment recovery: ~8 hours/month @ $75/hour = $600
- Total monthly cost: $2,440
After (Next.js):
- Vercel Pro hosting: $240/month
- GitHub Actions: $80/month (4-minute builds)
- Developer maintenance: ~4 hours/month = $300
- Total monthly cost: $620
Monthly savings: $1,820
Annual savings: $21,840
Migration cost: $15,000 (4 weeks @ $75/hour for 50 hours/week)
Payback period: 8.2 months
Developer Experience Improvements
Before: "I fucking hate deploying on Fridays because builds fail randomly"
After: "Deployment works, builds are fast, Sarah can publish content without my help"
The metrics that mattered:
- Build success rate: 53% → 97%
- Average build time: 47 minutes → 4 minutes
- Developer debugging time: 20 hours/month → 4 hours/month
- Content publishing time: 12 minutes → 30 seconds
- Memory-related crashes: 8/month → 0/month
Framework-Specific Migration Notes
Gatsby → Next.js (What We Did)
Pros: Both React-based, component code mostly portable, excellent documentation, large community
Cons: GraphQL → REST API conversion, plugin ecosystem replacement, different build-time concepts
Timeline: 4 weeks for 4,200 pages with complex data relationships
Effort distribution:
- 40% data fetching conversion
- 25% plugin replacement
- 20% image and asset migration
- 15% testing and debugging
Recommended for: Teams comfortable with React, existing Next.js knowledge, need for incremental static regeneration
Gatsby → Astro (Research Phase)
Pros: Fastest builds, component-agnostic, excellent for content sites
Cons: Smaller ecosystem, component architecture learning curve, fewer resources for complex data integration
Estimated timeline: 6-8 weeks (complete rewrite of components)
Best for: Content-heavy sites without complex interactivity
Gatsby → Nuxt (Research Phase)
Pros: Vue ecosystem, good SSR capabilities, growing community
Cons: Different component model, smaller plugin ecosystem than Next.js
Estimated timeline: 5-7 weeks (Vue learning curve for React team)
Best for: Teams already using Vue or wanting to escape React ecosystem
The Migration Checklist That Actually Works
Before You Start (Week -1)
- Audit current Gatsby plugins - identify Next.js equivalents
- Document all GraphQL queries and their data sources
- Inventory custom webpack configurations
- Map current URL structure to planned Next.js routes
- Set up Next.js project with matching folder structure
Week 1: Foundation
- Configure Next.js with all required plugins
- Set up development environment and build pipeline
- Convert 3-5 simple pages (home, about, contact)
- Implement basic routing and navigation
- Test deployment to staging environment
Week 2: Data Layer
- Build API routes for each data source
- Convert GraphQL queries to fetch/API calls
- Implement caching strategy for API responses
- Add error handling and loading states
- Test data accuracy against Gatsby site
Week 3: Features and Polish
- Migrate image optimization and handling
- Implement search functionality
- Add SEO meta tags and structured data
- Configure analytics and monitoring
- Set up incremental static regeneration
Week 4: Production Readiness
- Comprehensive testing (unit, integration, e2e)
- Performance auditing and optimization
- Set up monitoring and error tracking
- Configure CMS webhooks and content publishing
- Plan and execute production deployment
Week 5: Post-Launch (Budget for This!)
- Monitor for issues and user feedback
- Fix any overlooked bugs or regressions
- Optimize performance based on real usage data
- Train team on new deployment process
- Document changes and new workflows
The most important lesson: Budget 5 weeks, not 4. Something always breaks in production that didn't break in staging. Having that extra week approved upfront means you can fix issues properly instead of rushing patches.