performance2024-11-25·10 min·250/348

Next.js Build Memory: Solving JavaScript Heap Out of Memory

Why Next.js builds run out of memory and how to fix JavaScript heap out of memory errors with practical solutions

Next.js Build Memory: Solving JavaScript Heap Out of Memory

Introduction

Few errors are as frustrating as a build that crashes with JavaScript heap out of memory. You watch the build progress bar, it gets to 80%, and then the entire process dies. The error message is not helpful because it does not tell you what is consuming the memory or how to fix it.

I first encountered this issue on a project with 400+ pages and hundreds of thousands of lines of markdown content. The default Node.js heap allocation was nowhere near sufficient. This post walks through every strategy I used to resolve it.

Environment

  • OS: Windows 11
  • Node.js: v20.10.0
  • Next.js: 14.0.4
  • Available RAM: 32GB
  • Project Size: ~500 pages, 200K+ lines of content

Problem

Running next build produced:

<--- Last few GCs --->
[4824:000001E8A3C00000]  1234567 ms: Scavenge 4096.0 (4500.0) -> 4096.0 (4500.0) MB, 10.0 / 0.0 ms  (average = 10.0 ms, current = 10.0 ms) allocation failure
[4824:000001E8A3C00000]  1234600 ms: Mark-sweep 8192.0 (8500.0) -> 4096.0 (4500.0) MB, 200.0 / 0.0 ms  (average = 50.0 ms, current = 200.0 ms) allocation failure

<--- JS stacktrace --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

npm ERR! code ELIFECYCLE
npm ERR! errno 134

The build consumed over 8GB of RAM before crashing. Increasing the heap limit to 16GB delayed but did not prevent the crash.

Analysis

The memory consumption came from three sources:

1. Large data payloads in getStaticProps. Fetching thousands of records at build time and storing them in memory for page generation.

2. Image optimization. Processing hundreds of images simultaneously during the build creates massive memory pressure.

3. Webpack compilation. The default webpack configuration is not optimized for very large codebases.

4. Content parsing. Parsing markdown or MDX files with syntax highlighting plugins is extremely memory-intensive.

Solution

Fix 1: Increase the Node.js heap limit

// package.json
{
  "scripts": {
    "build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' next build"
  }
}
# Or run directly
set NODE_OPTIONS=--max-old-space-size=8192
next build

Fix 2: Optimize data fetching for static generation

// app/blog/[slug]/page.tsx
// Instead of fetching all posts at once, use on-demand generation
export const dynamicParams = true

export async function generateStaticParams() {
  // Only generate the most important pages at build time
  const posts = await getPopularPosts(100) // Limit to top 100
  return posts.map((post) => ({ slug: post.slug }))
}

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 },
  })
  return res.json()
}

Fix 3: Batch image optimization

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    // Limit concurrent image optimizations
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
    deviceSizes: [640, 750, 828, 1080],
    imageSizes: [16, 32, 48, 64, 96, 128, 256],
  },
  // Reduce webpack memory usage
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        ...config.resolve.fallback,
        fs: false,
      }
    }
    return config
  },
}

module.exports = nextConfig

Fix 4: Use dynamic imports for heavy components

// Instead of importing everything at the top
import { HeavyChart } from '@/components/HeavyChart'

// Use dynamic import with loading state
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => 
Loading chart...
, ssr: false, // Skip server rendering for heavy components })

Fix 5: Monitor and profile the build

# Enable Node.js heap snapshots
NODE_OPTIONS="--max-old-space-size=8192 --heapsnapshot-signal=SIGUSR2" next build

# Profile webpack compilation
NEXT_PROFILE_BUILD=1 next build
// scripts/build-monitor.js
const { execSync } = require('child_process')

function monitorBuild() {
  const startMem = process.memoryUsage()
  console.log('Starting build with memory:', startMem.heapUsed / 1024 / 1024, 'MB')

  try {
    execSync('next build', {
      stdio: 'inherit',
      env: {
        ...process.env,
        NODE_OPTIONS: '--max-old-space-size=8192',
      },
    })
  } catch (error) {
    console.error('Build failed:', error.message)
    process.exit(1)
  }
}

monitorBuild()

Lessons Learned

  1. 8GB heap limit is a band-aid, not a solution. If your build needs 8GB of RAM, something is structurally wrong with how data is being processed.

  2. Paginate data fetching at build time. Do not fetch all records into memory. Use cursor-based pagination and process data in streams.

  3. Use generateStaticParams with limits. Only pre-render the most important pages. Let ISR handle the rest.

  4. Profile before optimizing. Use --inspect flag to attach Chrome DevTools and identify actual memory consumers:

NODE_OPTIONS="--max-old-space-size=8192 --inspect" next build

Then open chrome://inspect in Chrome to connect to the build process.

  1. Consider splitting the build. For very large sites, build different sections separately using Next.js's output: 'standalone' mode.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.