performance2023-08-30·10 min·341/348

Fixing Slow HMR in Next.js Development Server

How to diagnose and resolve slow Hot Module Replacement in next dev for faster development feedback loops

Fixing Slow HMR in Next.js Development Server

Introduction

Hot Module Replacement (HMR) is what makes modern frontend development feel instant. You save a file, and the browser updates without losing component state. When HMR takes more than a second, the development flow is broken. You lose your train of thought, context switching becomes expensive, and productivity drops.

After experiencing HMR times of 5-10 seconds on a large Next.js project, I systematically identified every cause of slow HMR and implemented fixes that reduced update times to under 500ms.

Environment

  • OS: Windows 11
  • Node.js: v20.10.0
  • Next.js: 14.0.4
  • Disk: NVMe SSD
  • RAM: 32GB

Problem

The development server showed extremely slow HMR:

[WSM] File changed: components/Dashboard/Header.tsx
[WSM] Compiling client and server separately...
[WSM] Done in 5.2s

[WSM] File changed: lib/utils/helpers.ts
[WSM] Compiling...
[WSM] Done in 8.1s

Even simple changes like modifying a CSS class took 2-3 seconds. The dev server startup time was over 30 seconds.

Analysis

The causes of slow HMR were:

1. Large import trees. A single file change triggers recompilation of all dependent modules. Importing lodash entire library instead of individual functions causes massive recompilation.

2. TypeScript project references. Large tsconfig.json with include patterns that cover too many files slows type checking.

3. Webpack configuration. Custom webpack plugins and loaders add overhead to every compilation.

4. File system watchers. Windows file system watchers have different performance characteristics than Linux.

5. Next.js build cache. Corrupted .next cache forces full recompilation.

Solution

Fix 1: Optimize imports

// BEFORE - imports entire lodash library
import _ from 'lodash'
const result = _.debounce(handler, 300)

// AFTER - import specific functions
import debounce from 'lodash/debounce'
const result = debounce(handler, 300)

// OR use lodash-es for tree shaking
import { debounce } from 'lodash-es'
// BEFORE - barrel file import causes entire library to be included
import { Button, Card, Modal } from '@/components'

// AFTER - direct imports
import { Button } from '@/components/Button'
import { Card } from '@/components/Card'
import { Modal } from '@/components/Modal'

Fix 2: Speed up TypeScript checking

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,  // Skip type checking of .d.ts files
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",  // Faster than "node"
    "resolveJsonModule": true,
    "isolatedModules": true,
    "incremental": true,  // Enable incremental compilation
    "tsBuildInfoFile": ".next/tsconfig.tsbuildinfo"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules", ".next", "out", "dist"]
}

Fix 3: Speed up dev server startup

// package.json
{
  "scripts": {
    "dev": "next dev --turbopack"
  }
}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable experimental features for faster dev
  experimental: {
    // Use Turbopack for faster compilation
    // (Next.js 14+ with --turbopack flag)
  },

  // Reduce webpack overhead
  webpack: (config, { dev, isServer }) => {
    if (dev) {
      // Use faster source maps in development
      config.devtool = 'eval-cheap-module-source-map'
    }
    return config
  },
}

module.exports = nextConfig

Fix 4: Clear cache and restart cleanly

# Remove all Next.js build artifacts
rm -rf .next
rm -rf node_modules/.cache

# Clear npm cache
npm cache clean --force

# Restart dev server
npm run dev
# Windows PowerShell
Remove-Item -Recurse -Force .next
Remove-Item -Recurse -Force node_modules\.cache
npm run dev

Fix 5: Monitor HMR performance

# Enable webpack performance hints
NEXT_WEBPACK_PERF=1 next dev

# Profile the compilation
NODE_OPTIONS="--max-old-space-size=8192" next dev
// lib/hmr-monitor.ts
// Add to a development-only file to track HMR performance
if (process.env.NODE_ENV === 'development') {
  const originalFetch = window.fetch
  window.fetch = async (...args) => {
    const start = performance.now()
    const response = await originalFetch(...args)
    const duration = performance.now() - start

    if (args[0] && String(args[0]).includes('_next/')) {
      console.log(`[HMR] ${String(args[0]).split('/').pop()} - ${duration.toFixed(0)}ms`)
    }

    return response
  }
}

Lessons Learned

  1. Use --turbopack flag. Turbopack provides significantly faster HMR than the default webpack bundler. It is stable in Next.js 14.

  2. Avoid barrel files in development. Direct imports are always faster than importing from index files that re-export everything.

  3. Keep node_modules clean. Regularly remove unused packages. Each package adds to the module resolution graph.

  4. Use TypeScript incremental compilation. The incremental option in tsconfig.json caches type checking results between compilations.

  5. Monitor compilation time. Add timing logs to your development workflow to catch performance regressions early.

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