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:
| Column | What It Means |
|---|---|
| Constructor | The type of object (Array, Object, HTMLDivElement, etc.) |
| Distance | Steps from GC root—higher = more likely a leak |
| Shallow Size | Memory the object itself uses |
| Retained Size | Memory 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:
- Select the second snapshot
- Change the dropdown from "Summary" to "Comparison"
- Select the first snapshot to compare against
- 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:
- Select "Allocation instrumentation on timeline"
- Click Start
- Use your app normally for 30-60 seconds
- 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 See | What It Usually Means |
|---|---|
context in functionName() | Closure capturing variables |
EventListener | Forgot to call removeEventListener |
Promise | Unresolved promise holding references |
Map or Set | Growing cache without eviction |
timer_id in Window | Uncleared setInterval |
Exporting Memory Dumps for Analysis
For complex leaks, export the heap for offline analysis:
- Right-click the snapshot in the left panel
- Select "Save..."
- Save as
.heapsnapshotfile
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 loadother-chunk.js:123= dynamic importreact-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:
- Go to Sources panel
- Navigate to
webpack://or_N_E/(Next.js) - You can see the original, unminified source files
- 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:
| Color | What It Means |
|---|---|
| White | Queued—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:
- Network panel → click the chunk
- Response tab
- Click into the response content
Cmd+Ato select all,Cmd+Cto copy- 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 clipboardFor 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:
| Finding | Impact |
|---|---|
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 library | Security risk |
| Hardcoded API endpoints | Security 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
| Action | Shortcut |
|---|---|
| Take heap snapshot | Cmd+E (while in Memory panel) |
| Force garbage collection | Click the trash can icon |
| Search in snapshot | Cmd+F |
Network Panel Shortcuts
| Action | Shortcut |
|---|---|
| Clear network log | Cmd+K |
| Search across all responses | Cmd+F |
| Copy response | Right-click → Copy → Copy response |
| Filter by type | Type 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 elementThis is advanced debugging. For the fundamentals, start with Bundle Analysis and Memory Management.