Skip to content

Instantly share code, notes, and snippets.

@cassidoo
Last active June 2, 2026 03:42
Show Gist options
  • Select an option

  • Save cassidoo/63fa94bbfa73bc268304e2b9dafaa941 to your computer and use it in GitHub Desktop.

Select an option

Save cassidoo/63fa94bbfa73bc268304e2b9dafaa941 to your computer and use it in GitHub Desktop.
Diagnosing Slow Node.js / npm CLI Startup on Windows — systematic investigation guide

[System.Environment]::SetEnvironmentVariable("RAYFIN_TELEMETRY_OPTOUT", "1", "User")

Run in an elevated (Admin) PowerShell

Add-MpPreference -ExclusionPath "C:\nvm4w\nodejs\node_modules" Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\npm-cache" Add-MpPreference -ExclusionPath "$env:USERPROFILE.copilot\repos"

Diagnosing Slow Node.js / npm CLI Startup on Windows

You are a systems performance investigator. A developer reports that running npm scripts (e.g. npm run dev, npm run <cli-tool>) or CLI tools installed via npm takes minutes to return any output on Windows. Your job is to systematically diagnose and fix the root causes.

Investigation Methodology

Follow these steps in order. Each step includes the commands to run and what to look for.


Step 1: Establish Baselines

Time each layer independently to isolate where the delay lives:

# Layer 1: Raw process creation
Measure-Command { cmd /c "echo hello" } | Select-Object TotalSeconds
# Expected: <0.2s

# Layer 2: Node.js startup
Measure-Command { node -e "console.log('done')" } | Select-Object TotalSeconds
# Expected: <0.5s

# Layer 3: npm startup
Measure-Command { npm --version } | Select-Object TotalSeconds
# Expected: <2s

# Layer 4: npm run (pick a simple script)
Measure-Command { npm run --silent <script-name> } | Select-Object TotalSeconds
# If this is 10x+ slower than Layer 3, the problem is in script execution

# Layer 5: Direct tool execution (bypass npm)
Measure-Command { node node_modules/.bin/<tool> --version } | Select-Object TotalSeconds
# Compare to Layer 4 — if this is fast, npm overhead is the issue

# Layer 6: npx execution
Measure-Command { npx <tool> --version } | Select-Object TotalSeconds
# npx often does registry lookups — compare to Layer 5

Key insight: Run each command TWICE. If run 1 is 10-100x slower than run 2, you have a file scanning / caching problem (likely Windows Defender).


Step 2: Check Windows Defender Real-Time Protection

This is the #1 cause of slow Node.js tooling on Windows. Defender scans every .js file as Node loads it.

# Check if real-time protection is on
Get-MpComputerStatus | Select-Object RealTimeProtectionEnabled, OnAccessProtectionEnabled

# Check existing exclusions (requires admin)
Get-MpPreference | Select-Object ExclusionPath, ExclusionProcess

# Count JS files in the tool's dependency tree (shows scan scope)
(Get-ChildItem <tool-install-path>/node_modules -Recurse -Filter "*.js" -File).Count
# If this is 5,000+, Defender scanning is almost certainly the bottleneck

Fix (run in elevated/admin PowerShell):

# Exclude Node.js tooling paths from real-time scanning
Add-MpPreference -ExclusionPath "C:\path\to\nodejs\node_modules"     # global npm packages
Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\npm-cache"         # npm cache
Add-MpPreference -ExclusionPath "C:\path\to\your\project"             # project repos

# If using nvm/nvm4w/fnm, exclude the entire version manager directory
Add-MpPreference -ExclusionPath "C:\path\to\nvm\or\fnm"

Validation: Run the slow command twice again. Both runs should now be fast and consistent (no cold/warm variance).

⚠️ Prefer path exclusions over process exclusions (node.exe). Process exclusions bypass scanning for ALL files node touches, which is a security concern in corporate environments.


Step 3: Check CLI Telemetry

Many CLI tools initialize telemetry (OpenTelemetry, Application Insights, etc.) on every invocation, loading thousands of additional JS files and making network calls.

How to detect:

# Search the CLI's source for telemetry initialization
Select-String -Path "<tool-path>/dist/**/*.js" -Pattern "telemetry|initTelemetry|opentelemetry|appinsights" -Include "*.js"

# Search for opt-out environment variables
Select-String -Path "<tool-path>/dist/**/*.js" -Pattern "TELEMETRY.*OPT|OPT.*TELEMETRY|telemetry.*enabled|telemetry.*disabled" -Include "*.js"

Common opt-out env vars:

Tool Variable Value
Rayfin CLI RAYFIN_TELEMETRY_OPTOUT 1
Azure CLI AZURE_CORE_COLLECT_TELEMETRY 0
.NET CLI DOTNET_CLI_TELEMETRY_OPTOUT 1
Next.js NEXT_TELEMETRY_DISABLED 1
Gatsby GATSBY_TELEMETRY_DISABLED 1
OpenTelemetry (generic) OTEL_SDK_DISABLED true

Fix — set as persistent Windows user env var (works across ALL shells):

[System.Environment]::SetEnvironmentVariable("VARIABLE_NAME", "VALUE", "User")
$env:VARIABLE_NAME = "VALUE"  # also set in current session

Validation: Time the CLI with and without the opt-out to measure impact.


Step 4: Check npx vs npm run vs Direct Execution

npx is often much slower than npm run because it resolves packages from the registry.

# Check npm debug logs for registry fetches
$logDir = "$env:LOCALAPPDATA\npm-cache\_logs"
Get-ChildItem $logDir -Filter "*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content
# Look for: "http fetch GET" lines — these are registry lookups

Fix: Avoid npx for frequently-used tools. Use npm run scripts or direct execution instead:

// package.json — define scripts instead of using npx
{
  "scripts": {
    "mytool": "mytool",          // uses node_modules/.bin automatically
    "dev:fast": "vite"           // skip pre-hooks when config hasn't changed
  }
}

Step 5: Check for Slow Pre-Scripts

npm automatically runs pre<script> before <script>. These can silently double startup time.

# List all scripts to find hidden pre-hooks
npm run | Select-String "pre"

Example problem:

{
  "predev": "some-cli env --framework vite",  // runs BEFORE vite
  "dev": "vite"
}

Fix: Add a bypass script:

{
  "dev:fast": "vite",                          // skips predev
  "env": "some-cli env --framework vite"       // run manually when config changes
}

Step 6: Other Windows-Specific Checks

# DNS resolution speed (slow DNS affects npm registry + telemetry)
Measure-Command { [System.Net.Dns]::GetHostAddresses("registry.npmjs.org") } | Select-Object TotalSeconds
# Expected: <0.5s. If >2s, check DNS server config with: ipconfig /all

# PowerShell profile overhead
Measure-Command { pwsh -NoProfile -Command "exit" } | Select-Object TotalSeconds
Measure-Command { pwsh -Command "exit" } | Select-Object TotalSeconds
# Difference = profile overhead. Check $PROFILE for heavy init (oh-my-posh, etc.)

# PATH length (very long PATH slows process creation)
$env:PATH.Split(';').Count  # >50 entries can cause issues

# Check for corporate proxy (can slow telemetry/registry)
$env:HTTP_PROXY
$env:HTTPS_PROXY
docker info 2>&1 | Select-String -Pattern "proxy" -CaseSensitive:$false

Quick Fix Checklist

# Fix Impact Effort
1 Add Defender path exclusions for node_modules, npm cache, project repos 🔥 Highest Needs admin
2 Disable CLI telemetry via env var 🔥 High Low
3 Use npm run instead of npx Medium Low
4 Add dev:fast script to skip pre-hooks Medium Low
5 Fix slow DNS servers Medium Low
6 Optimize PowerShell profile Low Low

Validation Template

After applying fixes, run this validation script:

Write-Host "=== Post-Fix Validation ==="
Write-Host ""

Write-Host "1. Defender exclusions applied:"
# Requires admin:
# (Get-MpPreference).ExclusionPath | ForEach-Object { Write-Host "   $_" }

Write-Host ""
Write-Host "2. Telemetry opt-out:"
Write-Host "   RAYFIN_TELEMETRY_OPTOUT = $([System.Environment]::GetEnvironmentVariable('RAYFIN_TELEMETRY_OPTOUT', 'User'))"

Write-Host ""
Write-Host "3. CLI startup time (should be <5s):"
$t = Measure-Command { rayfin --version 2>&1 }
Write-Host "   rayfin --version: $($t.TotalSeconds)s"

Write-Host ""
Write-Host "4. Cold/warm consistency (both should be similar):"
$t1 = Measure-Command { npm run --silent lint 2>&1 }
Write-Host "   npm run lint (run 1): $($t1.TotalSeconds)s"
$t2 = Measure-Command { npm run --silent lint 2>&1 }
Write-Host "   npm run lint (run 2): $($t2.TotalSeconds)s"
$variance = [math]::Abs($t1.TotalSeconds - $t2.TotalSeconds)
Write-Host "   Variance: ${variance}s (should be <5s)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment