Back to Blog
D

Debugging Next.js 16 Turbopack Memory Leak and CPU Spin: A Complete Investigation from Browser to Node.js Process

April 22, 2026·12 min readFeatured
Next.jsTurbopackDebuggingNode.js

Table of Contents

  • The Problem
  • Round 1: Suspecting Browser-Side Components
  • Initial Analysis
  • Issues Found
  • Round 1 Fixes
  • Result: Problem Persists
  • Round 2: Shifting to CSS Rendering Pipeline
  • Analyzing the Rendering Pipeline
  • Round 2 Fixes
  • Critical Turning Point
  • Round 3: Global Components + Process Monitoring
  • Investigating All-Pages Shared Components
  • The Decisive Step: Monitoring the Node.js Process
  • Root Cause: Official Turbopack Bug
  • Known Official Issues
  • Technical Root Cause
  • Final Fix
  • Lessons Learned
  • Evolution of Debugging Approach
  • Key Takeaways
  • Bonus Benefits

Table of Contents

  • The Problem
  • Round 1: Suspecting Browser-Side Components
  • Initial Analysis
  • Issues Found
  • Round 1 Fixes
  • Result: Problem Persists
  • Round 2: Shifting to CSS Rendering Pipeline
  • Analyzing the Rendering Pipeline
  • Round 2 Fixes
  • Critical Turning Point
  • Round 3: Global Components + Process Monitoring
  • Investigating All-Pages Shared Components
  • The Decisive Step: Monitoring the Node.js Process
  • Root Cause: Official Turbopack Bug
  • Known Official Issues
  • Technical Root Cause
  • Final Fix
  • Lessons Learned
  • Evolution of Debugging Approach
  • Key Takeaways
  • Bonus Benefits

The Problem

While developing my personal website, I noticed something strange: after running npm run dev, opening the homepage and leaving it idle would cause memory usage to climb continuously. Even more puzzling, CPU usage spiked to 100%.

Round 1: Suspecting Browser-Side Components

Initial Analysis

I first focused on all visible components on the homepage. A typical portfolio hero screen contains these dynamic elements:

ComponentDynamic BehaviorSuspicion Level
FloatingParticlesCanvas particle animation, creating/destroying objects per frame⭐⭐⭐⭐⭐
HeroSectionTypewriter effect, ~12.5 re-renders/sec⭐⭐⭐⭐
MouseGlowContinuous requestAnimationFrame loop⭐⭐⭐
TiltCardMouse-follow tilt effect⭐⭐

Issues Found

1. FloatingParticles: Creating Garbage Every Frame

The original implementation performed these operations every single frame:

// filter() creates a new array each frame
particles = particles.filter(p => p.life > 0);

// Dead particles replaced with new objects
if (particles.length < maxParticles) {
  particles.push(createParticle()); // New object allocation
}

This means 60 frames/sec × N particles = massive numbers of short-lived objects, putting enormous pressure on the GC. Additionally, font sizes used random floating-point numbers:

const fontSize = Math.random() * 20 + 10; // 10.0001, 15.2345...

Each unique float value causes the browser to create a new font cache entry—the font cache grows infinitely.

2. HeroSection: Typing Effect Cascading Re-renders

The useTypingEffect Hook updates state every 80ms, causing the entire HeroSection component tree (including AnimatedName, TechTags, SocialLinks, etc.) to re-render completely. None of the child components used React.memo for optimization.

3. TiltCard: Missing RAF Cleanup

The component didn't call cancelAnimationFrame on unmount, so animation frame callbacks continued executing after the component was removed.

4. Header: fetch Without AbortController

The authentication request had no AbortController, meaning fetch could still be in progress after component unmount.

Round 1 Fixes

I made the following changes:

  • FloatingParticles: Switched to object pool pattern—particles are reused via an alive flag instead of being destroyed/recreated; predefined 10 integer font sizes; added font string cache Map
  • HeroSection: Extracted TypingText as a separate component; wrapped AnimatedName and TechTags with React.memo
  • TiltCard: Added useEffect cleanup for RAF
  • Header: Added AbortController to fetch

Result: Problem Persists

After restarting the project, CPU was still at 100%. While Round 1's fixes reduced browser-side memory allocation and unnecessary re-renders, they clearly weren't the root cause.

Round 2: Shifting to CSS Rendering Pipeline

Since browser-side JavaScript wasn't the main culprit, could it be CSS rendering?

Analyzing the Rendering Pipeline

The homepage had multiple layers of visual effects stacked together:

  1. MouseGlow: blur-[100px] massive blur glow + 60fps RAF loop
  2. hero-gradient-bg: background-position animation (cannot be GPU-composited)
  3. dot-matrix-glass: backdrop-filter: saturate(50%) blur(4px) (frosted glass effect)

These effects formed a rendering pipeline vicious cycle: RAF triggers repaint → backdrop-filter forces CPU composition → background-position animation triggers full-layer repaint → blur re-composes → cycle repeats.

Round 2 Fixes

  • MouseGlow: Removed continuous RAF loop, only updates on mousemove events
  • hero-gradient-bg: Changed background-position animation to transform: translate3d() animation (GPU-compositable)
  • dot-matrix-glass: Replaced backdrop-filter with semi-transparent background color

Critical Turning Point

This time I reverted the code. The reason was discovering a crucial clue:

Even on the blog list page (not the homepage), CPU usage was still at 100%!

This meant the problem wasn't specific to any particular page's rendering effects—it was global. Round 2's direction was wrong, so I immediately reverted all changes.

Round 3: Global Components + Process Monitoring

Investigating All-Pages Shared Components

By analyzing the route structure, I found that all pages were wrapped in PortfolioPageWrapper, which contains these global components:

  • MouseGlow — Global mouse glow
  • Header — Navigation bar (with fetch auth requests)
  • Footer — Page footer
  • ChatWidget — AI chat widget (with setInterval typewriter, useSyncExternalStore)

After reviewing each one individually, while they had room for optimization, none could explain 100% CPU.

The Decisive Step: Monitoring the Node.js Process

Since I couldn't find the cause in the browser, I turned my attention to the server-side process. Using PowerShell to monitor the Node.js process resource usage:

# Turbopack mode
Get-Process node | Select-Object Id, CPU, WorkingSet64
# Result: PID 38420, CPU: 107%, Memory: 2755MB → 3089MB (growing)

# Contrast: Webpack mode
Get-Process node | Select-Object Id, CPU, WorkingSet64  
# Result: PID 4748, CPU: ~0%, Memory: 1016MB (stable)

The data was crystal clear:

ModeCPU UsageMemory UsageTrend
Turbopack107%2755 → 3089 MB⬆️ Continuously growing
Webpack~0%1016 MB➡️ Stable

Root Cause: Official Turbopack Bug

After three rounds of investigation, the final conclusion was clear: the culprit behind 100% CPU and continuous memory growth is a Bug in Next.js 16's Turbopack development server itself.

Known Official Issues

Multiple related issues exist on GitHub:

IssueDescriptionVersion
#92246Turbopack dev server OOM: heap grows to 17GB+ when idle16.2.1
#93069Memory leak fix merged in canary but not released to 16.x stable after 45 days16.2.4
#86893next-server becomes zombie with 400-700% CPU-
#87322Turbopack infinite compilation loop-

Technical Root Cause

Two core fix PRs have been merged into the canary branch but are not included in the 16.2.4 stable release:

  1. PR #88577: tee'd response clones weren't registered with FinalizationRegistry, causing ArrayBuffer/WriteWrap leaks
  2. PR #89040: LRU cache entries had no minimum size limit, allowing unbounded growth

Final Fix

The solution was surprisingly simple—switch to Webpack mode in package.json:

{
  "scripts": {
    "dev": "next dev --webpack"
  }
}

Verification result: CPU dropped from 107% to ~0%, memory stabilized from 3GB+ and growing to ~837MB.

Lessons Learned

Evolution of Debugging Approach

Round 1: Browser JS layer → Found real issues but not the root cause
    ↓
Round 2: Browser CSS rendering layer → Wrong direction, corrected by user feedback
    ↓ 
Round 3: Global components + Server-side process → Found the true root cause

Key Takeaways

  1. Think across layers: When single-layer investigation hits dead ends, dare to cross boundaries (browser → Node.js process)
  2. Control variable verification: The observation that "blog list page also shows 100% CPU" was the critical turning point, ruling out page-specific factors
  3. Let data speak: Don't guess—directly measure process-level CPU/memory metrics, data doesn't lie
  4. Official bugs are common: Even mature frameworks' latest versions can have serious bugs—check the Issue Tracker first when encountering anomalies
  5. Revert quickly: When you discover your fix direction is wrong, revert decisively to avoid introducing unnecessary changes

Bonus Benefits

While the Turbopack Bug was the primary cause, Round 1's browser-side optimizations remain valuable improvements:

  • Object pool pattern reduces GC pressure
  • React.memo reduces unnecessary re-renders
  • RAF cleanup prevents memory leaks
  • AbortController prevents dangling requests

These optimizations are equally effective in production environments (built with Webpack).