Turbopack Production Ready: My Migration Experience
Our Next.js build took 8 minutes. After migrating to Turbopack in Next.js 16, it takes 2 minutes and 40 seconds. That's a 3x improvement with zero code changes. But the migration wasn't entirely smooth—we hit three significant issues. Here's the complete story.
Why Turbopack Matters
Turbopack replaces Webpack as Next.js's bundler. Written in Rust, it parallelizes work more efficiently and caches aggressively. The performance gains are substantial:
| Metric | Webpack | Turbopack | Improvement | |--------|---------|-----------|-------------| | Cold build | 8m 12s | 2m 40s | 3x faster | | Dev startup | 45s | 8s | 5.6x faster | | HMR update | 2.1s | 180ms | 11.7x faster | | Memory usage | 4.2GB | 1.8GB | 2.3x less |
These numbers are from our production application: 450 components, 200 routes, 180 dependencies.
The Migration Path
Next.js 16 makes Turbopack the default. If you're upgrading, you're automatically on Turbopack unless you opt out:
// next.config.js - opt out if needed
module.exports = {
turbo: false, // Falls back to Webpack
};For existing projects, the upgrade command handles most transitions:
npx next@latest upgradeIssue 1: Custom Webpack Loaders
Our first build failed immediately:
Error: Cannot find module './svgr-loader'We used @svgr/webpack to import SVGs as React components:
// Old next.config.js
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};Turbopack doesn't support Webpack loaders. The solution was migrating SVGs to a different pattern:
// Before: SVGR loader
import Logo from './logo.svg';
<Logo className="h-8 w-8" />
// After: React component
// components/icons/Logo.tsx
export function Logo({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="..." />
</svg>
);
}
// Or use next/image for static SVGs
import Image from 'next/image';
<Image src="/logo.svg" alt="Logo" width={32} height={32} />We created a script to automate the conversion:
// scripts/convert-svgs.js
const fs = require('fs');
const path = require('path');
const { transform } = require('@svgr/core');
const svgDir = './public/icons';
const outputDir = './components/icons';
fs.readdirSync(svgDir).forEach(async (file) => {
if (!file.endsWith('.svg')) return;
const svg = fs.readFileSync(path.join(svgDir, file), 'utf8');
const componentName = file.replace('.svg', '').replace(/-./g, x => x[1].toUpperCase());
const component = await transform(svg, {
typescript: true,
plugins: ['@svgr/plugin-jsx'],
});
fs.writeFileSync(
path.join(outputDir, `${componentName}.tsx`),
component
);
});Issue 2: CSS Module Composition
We used CSS Module composition extensively:
/* styles/buttons.module.css */
.base {
padding: 8px 16px;
border-radius: 4px;
}
.primary {
composes: base;
background: blue;
color: white;
}Turbopack initially had issues with complex composes chains. The fix was simplifying composition:
/* Simplified approach */
.base {
padding: 8px 16px;
border-radius: 4px;
}
.primary {
padding: 8px 16px;
border-radius: 4px;
background: blue;
color: white;
}Or better, we migrated shared styles to Tailwind utilities:
// Using Tailwind instead of CSS Modules composition
<button className="px-4 py-2 rounded bg-blue-600 text-white">
Primary Button
</button>Issue 3: Dynamic Imports with Complex Paths
Our barrel exports caused issues:
// lib/components/index.ts
export { Button } from './Button';
export { Card } from './Card';
export { Modal } from './Modal';
// ... 50 more exports
// Usage
const { Modal } = await import('@/lib/components');Turbopack struggled with tree-shaking these dynamic imports. The solution was direct imports:
// Instead of barrel import
const Modal = await import('@/lib/components/Modal');
// Or static import with React.lazy
const Modal = lazy(() => import('@/lib/components/Modal'));Performance Monitoring
We tracked build performance during migration:
// scripts/build-metrics.js
const { execSync } = require('child_process');
const fs = require('fs');
const start = Date.now();
execSync('next build', { stdio: 'inherit' });
const duration = Date.now() - start;
const metrics = JSON.parse(
fs.readFileSync('.next/build-metrics.json', 'utf8') || '{}'
);
console.log({
totalDuration: `${(duration / 1000).toFixed(1)}s`,
...metrics,
});Incremental Migration Strategy
We didn't migrate everything at once:
Week 1: Development Mode Only
module.exports = {
// Use Turbopack only in development
turbo: process.env.NODE_ENV === 'development',
};This let developers experience Turbopack's fast HMR while production builds remained on Webpack.
Week 2: Fix Development Issues
We addressed SVG imports and CSS Module issues that appeared during development.
Week 3: Production Builds
With development stable, we enabled Turbopack for production:
module.exports = {
turbo: true,
};Week 4: Monitoring and Optimization
We monitored production deploys for any runtime issues and optimized remaining slow paths.
Turbopack-Specific Optimizations
Once on Turbopack, we leveraged its features:
Persistent Caching
Turbopack caches build artifacts more aggressively. Our CI benefited from cache reuse:
# .github/workflows/build.yml
- name: Cache Turbopack
uses: actions/cache@v3
with:
path: |
.next/cache
node_modules/.cache/turbopack
key: turbopack-${{ hashFiles('**/package-lock.json') }}-${{ github.sha }}
restore-keys: |
turbopack-${{ hashFiles('**/package-lock.json') }}-Parallel Processing
Turbopack parallelizes more work. We ensured our CI had sufficient CPU:
jobs:
build:
runs-on: ubuntu-latest-16-cores # More cores = faster buildsDevelopment Experience Improvements
The biggest win was development velocity:
Before (Webpack)
- Start dev server: 45 seconds
- Change a component: 2.1 second HMR
- Change a page: 3.5 second refresh
After (Turbopack)
- Start dev server: 8 seconds
- Change a component: 180ms HMR
- Change a page: 400ms refresh
Developers no longer waited for builds. The feedback loop tightened dramatically.
Compatibility Checklist
Before migrating, verify:
- [ ] No custom Webpack plugins in
next.config.js - [ ] No custom Webpack loaders (especially for SVG, GraphQL)
- [ ] CSS Modules don't use complex
composeschains - [ ] Dynamic imports use direct paths, not barrel exports
- [ ] No webpack-specific environment variables
If any apply, plan remediation before enabling Turbopack.
Rollback Plan
Keep a rollback path:
// next.config.js
const useTurbopack = process.env.USE_TURBOPACK !== 'false';
module.exports = {
turbo: useTurbopack,
};# Quick rollback via environment variable
USE_TURBOPACK=false npm run buildWhen to Stay on Webpack
Some scenarios still favor Webpack:
- Heavy reliance on Webpack plugins with no Turbopack equivalent
- Custom loaders that can't be replaced
- Monorepos with complex shared configurations
- Very large legacy codebases where migration cost exceeds benefit
For most Next.js applications, especially greenfield projects, Turbopack is the clear choice.
Results Summary
Three weeks of migration work yielded:
- 3x faster production builds: 8 minutes → 2.5 minutes
- 5x faster dev startup: 45 seconds → 8 seconds
- 10x faster HMR: 2 seconds → 180ms
- 50% less memory: Better CI performance and cost
The investment paid off within a week through developer productivity gains. Turbopack in Next.js 16 is ready for production—the migration just requires planning for Webpack-specific patterns.