perf hacks

Advanced DevTools Debugging

Chrome's hidden superpowers for performance forensics. Memory dumps, chunk analysis, and using LLMs to decode what's actually in your bundles.

The Advanced Toolkit

The bundle analyzer shows you what is big. DevTools shows you why it's there, when it loads, and what's inside. This is forensic-level debugging.

Part 1: Memory Forensics

When your app gets sluggish over time, the problem is usually memory. Chrome's Memory panel lets you take X-rays of your heap.

Taking Heap Snapshots

Open Chrome DevTools

Press F12 or Cmd+Option+I (Mac) / Ctrl+Shift+I (Windows)

Navigate to the Memory tab

Select "Heap snapshot" as the profiling type

Take a baseline snapshot

Click "Take snapshot" when your app is in a known good state (e.g., just loaded)

Perform the suspected action

Navigate to a heavy page, open a modal, trigger the behavior that seems slow

Take a comparison snapshot

Take another snapshot after the action. Now you can compare.

Reading Heap Snapshots

The snapshot view has four columns that matter:

ColumnWhat It Means
ConstructorThe type of object (Array, Object, HTMLDivElement, etc.)
DistanceSteps from GC root—higher = more likely a leak
Shallow SizeMemory the object itself uses
Retained SizeMemory that would be freed if this object was GC'd

The trick: Sort by "Retained Size" descending. The biggest retained objects are your suspects.

Finding Memory Leaks

After taking two snapshots:

  1. Select the second snapshot
  2. Change the dropdown from "Summary" to "Comparison"
  3. Select the first snapshot to compare against
  4. Look at the "# Delta" column—positive numbers mean new objects that weren't garbage collected
Objects that show up as +50, +100, +500 are your leak candidates.
Click to expand and see the retainer tree—what's keeping them alive?

For leaks that happen gradually:

  1. Select "Allocation instrumentation on timeline"
  2. Click Start
  3. Use your app normally for 30-60 seconds
  4. Click Stop

Blue bars = allocated. Gray bars = garbage collected.

Bars that stay blue = memory that's never freed = leaks.

Click any blue bar to see what was allocated at that moment.

Common Leak Patterns in the Heap

When you expand an object in the retainer tree, you'll see what's keeping it alive:

Pattern You SeeWhat It Usually Means
context in functionName()Closure capturing variables
EventListenerForgot to call removeEventListener
PromiseUnresolved promise holding references
Map or SetGrowing cache without eviction
timer_id in WindowUncleared setInterval

Exporting Memory Dumps for Analysis

For complex leaks, export the heap for offline analysis:

  1. Right-click the snapshot in the left panel
  2. Select "Save..."
  3. Save as .heapsnapshot file

You can reimport these later, share with teammates, or feed to analysis tools.

Pro Tip: Record Allocation Stacks

Before profiling, enable "Record allocation stacks" in the Memory panel settings. This shows you exactly which function allocated each object—invaluable for tracking down the source of leaks.


Part 2: Network Chunk Investigation

Your bundle is split into chunks. The Network panel shows exactly what's in each one—and when it loads.

Finding Your Chunks

Open Network panel

Filter by "JS" to see only JavaScript files

Reload the page

Watch the waterfall. The first chunks are your critical path.

Look at the "Initiator" column

Shows what triggered each chunk to load. Look for:

  • (index) = initial load
  • other-chunk.js:123 = dynamic import
  • react-dom = React lazy loading

Inspecting Chunk Contents

Here's where it gets powerful. You can see exactly what's in each chunk:

Click on any chunk in the Network panel

Go to the "Response" tab

This shows the raw chunk content. It's minified, but searchable.

Go to the "Preview" tab

Chrome attempts to pretty-print it. More readable but still minified.

Use Cmd+F to search within the chunk

Search for:

  • Library names (lodash, moment, @mui)
  • Your component names
  • Specific function names
  • Import paths (node_modules/)

The Source Maps Trick

If source maps are enabled (they should be in dev), you get superpowers:

  1. Go to Sources panel
  2. Navigate to webpack:// or _N_E/ (Next.js)
  3. You can see the original, unminified source files
  4. Right-click any file → "Reveal in Network panel" to see which chunk it landed in

Chunk Load Timing Analysis

In the Network panel, look at the Waterfall column:

ColorWhat It Means
WhiteQueued—waiting for connection
Gray (Stalled)Browser is rate-limiting
Green (TTFB)Waiting for first byte from server
Blue (Content Download)Actually downloading the bytes

If you see long gray/white bars: You're loading too many chunks in parallel. Browser limits connections.

If you see long blue bars: Chunk is too big. Split it further.


Part 3: Extracting Chunk Content for LLM Analysis

This is the advanced technique: extracting minified chunk content and using an LLM to decode what's actually in there.

Why This Works

Minified code is hard for humans but trivial for LLMs. They can:

  • Identify which libraries are present
  • Estimate the size contribution of each library
  • Spot duplicated code across chunks
  • Identify code that shouldn't be in the client bundle

Extraction Method

Quick and dirty for one-off investigation:

  1. Network panel → click the chunk
  2. Response tab
  3. Click into the response content
  4. Cmd+A to select all, Cmd+C to copy
  5. Paste into your LLM

Prompt template:

This is a minified JavaScript bundle from a Next.js application.
Please analyze it and tell me:

1. What major libraries/frameworks are included?
2. Approximately what percentage of the bundle is each library?
3. Is there anything that looks like it shouldn't be in a client bundle
   (server-only code, API keys, large data objects)?
4. Are there any obvious duplications or inefficiencies?

Here's the bundle:
[paste minified code]

For systematic analysis, extract chunks via script:

// Run this in the Console after page load
const chunks = performance.getEntriesByType('resource')
  .filter(r => r.initiatorType === 'script' && r.name.includes('.js'))
  .map(r => ({
    name: r.name.split('/').pop(),
    size: r.transferSize,
    url: r.name,
  }));

// Log chunk info
console.table(chunks);

// Fetch and save a specific chunk
async function extractChunk(url) {
  const response = await fetch(url);
  const text = await response.text();
  console.log(`Chunk size: ${(text.length / 1024).toFixed(2)} KB`);
  return text;
}

// Example: extract the largest chunk
const largest = chunks.sort((a, b) => b.size - a.size)[0];
const content = await extractChunk(largest.url);
copy(content); // Copies to clipboard

For CI integration, analyze during build:

// scripts/analyze-chunks.js
const fs = require('fs');
const path = require('path');

const buildDir = '.next/static/chunks';
const chunks = fs.readdirSync(buildDir)
  .filter(f => f.endsWith('.js'))
  .map(f => ({
    name: f,
    size: fs.statSync(path.join(buildDir, f)).size,
    content: fs.readFileSync(path.join(buildDir, f), 'utf8'),
  }))
  .sort((a, b) => b.size - a.size);

// Output largest chunks for analysis
chunks.slice(0, 5).forEach(chunk => {
  console.log(`\n=== ${chunk.name} (${(chunk.size / 1024).toFixed(2)} KB) ===`);

  // Sample: first 500 chars to identify what's in it
  console.log(chunk.content.substring(0, 500) + '...');
});

LLM Analysis Prompts

Different prompts for different investigations:

Finding Library Bloat:

Analyze this minified JS chunk and identify:
1. All third-party libraries (look for common patterns like __webpack_require__,
   known library function names, etc.)
2. Which library appears to take the most space
3. Any libraries that seem duplicated or have multiple versions

[paste chunk]

Finding Leaked Server Code:

This should be a client-side JavaScript bundle. Look for anything that
suggests server-side code leaked into it:
- Database queries or ORM code (prisma, drizzle, knex)
- Environment variable access beyond NEXT_PUBLIC_*
- File system operations
- Server-only imports

[paste chunk]

Bundle Archaeology:

I'm trying to understand why this chunk is so large. Please:
1. Identify the main purpose of this chunk (vendor libs? app code? specific feature?)
2. List everything you can identify in it
3. Estimate rough percentage breakdown by library/purpose
4. Suggest what could potentially be removed or lazy-loaded

[paste chunk]

What the LLM Can Find

Real examples of what LLM analysis reveals:

FindingImpact
Full Lodash when only debounce used-60KB
Moment.js locales included-200KB
Entire AWS SDK in client bundle-500KB+
React DevTools code in production-50KB
Duplicate React versions-40KB
Server-only validation librarySecurity risk
Hardcoded API endpointsSecurity issue

Part 4: Putting It All Together

Here's the full forensic workflow for a slow app:

Baseline Memory Snapshot

Take a heap snapshot on initial load. Note the total heap size.

Profile the Problem Flow

Navigate through the slow flow. Take snapshots at key points.

Compare Snapshots

Use comparison view to find objects that accumulate. These are leak candidates.

Network Chunk Audit

Reload with Network panel open. Identify the largest chunks and when they load.

Extract and Analyze

Copy the largest chunks and feed to an LLM. Get a breakdown of what's inside.

Target the Biggest Wins

Based on analysis, prioritize:

  • Removing unused libraries
  • Lazy-loading heavy chunks
  • Fixing memory leaks
  • Splitting monolithic chunks

Quick Reference

Memory Panel Shortcuts

ActionShortcut
Take heap snapshotCmd+E (while in Memory panel)
Force garbage collectionClick the trash can icon
Search in snapshotCmd+F

Network Panel Shortcuts

ActionShortcut
Clear network logCmd+K
Search across all responsesCmd+F
Copy responseRight-click → Copy → Copy response
Filter by typeType js or script in filter box

Console Snippets

// List all loaded scripts with sizes
performance.getEntriesByType('resource')
  .filter(r => r.initiatorType === 'script')
  .map(r => ({ name: r.name.split('/').pop(), kb: (r.transferSize/1024).toFixed(1) }))
  .sort((a, b) => b.kb - a.kb);

// Total JS loaded
performance.getEntriesByType('resource')
  .filter(r => r.initiatorType === 'script')
  .reduce((sum, r) => sum + r.transferSize, 0) / 1024;

// Find what's keeping an object alive (run in Console)
// Select element in Elements panel first, then:
// queryObjects(Constructor) - finds all instances
// $0 - references selected element

This is advanced debugging. For the fundamentals, start with Bundle Analysis and Memory Management.

On this page