Modify your scraper to run longer and generate more load:
// Add this to your test script
const longRunningUrls = Array(100).fill('https://example.com');
// This gives Instruments enough data to capture patterns# Open Instruments
open -a "Instruments"Or from Xcode:
Xcode β Open Developer Tool β Instruments
- Select Allocations instrument
- Click Choose Target
- Look for "Google Chrome Helper (Renderer)" processes
- Pro Tip: Choose the one with highest CPU usage (active tab)
This is the most powerful feature for identifying leaks:
- Start recording in Instruments
- Run your scraper
- After 10 pages, click Mark Generation (button at bottom)
- Let it process 50 more pages
- Click Mark Generation again
- Stop recording
What to look for:
- Objects created in Gen 1 that persist into Gen 2 = potential leak
- Look for "VM Region" and "Dirty Memory" growth
- Check "All Heap & Anonymous VM" for overall trend
| Metric | What It Means | Good Range |
|---|---|---|
| All Heap | JavaScript objects | Should stabilize |
| Dirty Size | Non-swappable memory | Watch for growth |
| Anonymous VM | Malloc'd memory | Should be stable |
| CG backing stores | Image/canvas buffers | Block these! |
-
Click the Call Tree button
-
Check these checkboxes:
- Separate by Thread
- Invert Call Tree
- Hide System Libraries
-
Look for:
page.evaluate()calls creating large objects- Array growth in scraping logic
- Event listeners not being removed
Problem: Memory grows 10MB per 100 pages
Instruments shows:
Gen 1: 50MB baseline
Gen 2 (after 100 pages): 1.2GB (!!)
Call Tree reveals:
800MB β [JavaScript] parseData
β Array.push() β Storing all results in memory!
Solution: Stream results to disk instead of array
When profiling Chrome, you'll see these categories:
-
V8 Heap - JavaScript objects
- DOM nodes
- Scraped data
- Closure references
-
GPU Memory - Rendering buffers
- Canvas/bitmaps
- Should be minimal with resource blocking
-
Compressed Memory - macOS swap
- High values = system swapping to SSD
-
Wired Memory - Kernel-resident
- Browser executable
- Should be stable
# Find Chrome renderer PID
pgrep -f "Chrome Helper.*Renderer"
# Attach Instruments from command line
instruments -t "Allocations" -D chrome_profile.trace \
-p $(pgrep -f "Chrome Helper.*Renderer")
# Then run your scraper in another terminalSteady climb without plateaus
β
1GB β β±βββββββ
β β±βββββ±
β β±βββββ±
β β±βββββ±
0GB ββββββββββββββββββββββ> Time
Sawtooth with cleanup
β
500M β β±β² β±β² β±β²
β β± β² β± β² β± β²
β β± β²β± β²β± β²
0GB ββββββββββββββββββββββ> Time
- Use Generation Analysis - It filters out noise and shows only new allocations
- Profile with realistic load - 100+ pages minimum
- Check both processes - Profile both browser and renderer
- Correlate with logs - Add memory logging to your code
- Baseline first - Profile empty run to establish baseline
Use alongside Allocations to see swap usage:
High "Swap Size" = macOS is compressing memory to SSD
This = Performance death knell
For CPU-bound memory issues:
High CPU + Growing memory = Infinite loop creating objects
Add memory markers in your code:
console.log('PROFILE: page-start');
await page.goto(url);
console.log('PROFILE: page-end');
await page.close();
console.log('PROFILE: page-closed');Then correlate timestamps in Instruments with your console output.
- Result accumulation - Not streaming scraped data
- Event listeners - Not removing page listeners
- Context leaks - Not closing browser contexts
- Cache buildup - Not clearing browser caches
- Screenshot buffers - Accumulating in memory