
Chrome Devtools
- 2 installs
- 1 repo stars
- Updated October 28, 2025
- mrgoonie/xxxnaper
Automate a browser with Puppeteer CLI scripts to take screenshots, monitor network, analyze performance, scrape, and debug JavaScript, all outputting JSON.
About
Provides executable Puppeteer scripts for browser automation, screenshots, performance analysis, network monitoring, scraping, and form automation. A developer uses it to script Chrome tasks with JSON output for easy parsing.
- JSON-outputting Puppeteer scripts with a Linux/WSL dependency installer
- Covers screenshots, performance, network monitoring, and scraping
Chrome Devtools by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,842 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 26, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mrgoonie/xxxnaper --skill chrome-devtoolsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 1 |
| Last updated | October 28, 2025 |
| Repository | mrgoonie/xxxnaper ↗ |
What it does
Automate a browser with Puppeteer CLI scripts to take screenshots, monitor network, analyze performance, scrape, and debug JavaScript, all outputting JSON.
Files
Chrome DevTools Agent Skill
Browser automation via executable Puppeteer scripts. All scripts output JSON for easy parsing.
Quick Start
Installation
Step 1: Install System Dependencies (Linux/WSL only)
On Linux/WSL, Chrome requires system libraries. Install them first:
cd .claude/skills/chrome-devtools/scripts
./install-deps.sh # Auto-detects OS and installs required libsSupports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro
macOS/Windows: Skip this step (dependencies bundled with Chrome)
Step 2: Install Node Dependencies
npm install # Installs puppeteer, debug, yargsTest
node navigate.js --url https://example.com
# Output: {"success": true, "url": "https://example.com", "title": "Example Domain"}Available Scripts
All scripts are in .claude/skills/chrome-devtools/scripts/
Script Usage
./scripts/README.md
Core Automation
navigate.js- Navigate to URLsscreenshot.js- Capture screenshots (full page or element)click.js- Click elementsfill.js- Fill form fieldsevaluate.js- Execute JavaScript in page context
Analysis & Monitoring
snapshot.js- Extract interactive elements with metadataconsole.js- Monitor console messages/errorsnetwork.js- Track HTTP requests/responsesperformance.js- Measure Core Web Vitals + record traces
Usage Patterns
Single Command
cd .claude/skills/chrome-devtools/scripts
node screenshot.js --url https://example.com --output ./docs/screenshots/page.pngImportant: Always save screenshots to ./docs/screenshots directory.
Chain Commands (reuse browser)
# Keep browser open with --close false
node navigate.js --url https://example.com/login --close false
node fill.js --selector "#email" --value "user@example.com" --close false
node fill.js --selector "#password" --value "secret" --close false
node click.js --selector "button[type=submit]"Parse JSON Output
# Extract specific fields with jq
node performance.js --url https://example.com | jq '.vitals.LCP'
# Save to file
node network.js --url https://example.com --output /tmp/requests.jsonCommon Workflows
Web Scraping
node evaluate.js --url https://example.com --script "
Array.from(document.querySelectorAll('.item')).map(el => ({
title: el.querySelector('h2')?.textContent,
link: el.querySelector('a')?.href
}))
" | jq '.result'Performance Testing
PERF=$(node performance.js --url https://example.com)
LCP=$(echo $PERF | jq '.vitals.LCP')
if (( $(echo "$LCP < 2500" | bc -l) )); then
echo "✓ LCP passed: ${LCP}ms"
else
echo "✗ LCP failed: ${LCP}ms"
fiForm Automation
node fill.js --url https://example.com --selector "#search" --value "query" --close false
node click.js --selector "button[type=submit]"Error Monitoring
node console.js --url https://example.com --types error,warn --duration 5000 | jq '.messageCount'Script Options
All scripts support:
--headless false- Show browser window--close false- Keep browser open for chaining--timeout 30000- Set timeout (milliseconds)--wait-until networkidle2- Wait strategy
See ./scripts/README.md for complete options.
Output Format
All scripts output JSON to stdout:
{
"success": true,
"url": "https://example.com",
... // script-specific data
}Errors go to stderr:
{
"success": false,
"error": "Error message"
}Finding Elements
Use snapshot.js to discover selectors:
node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'Troubleshooting
Common Errors
"Cannot find package 'puppeteer'"
- Run:
npm installin the scripts directory
"error while loading shared libraries: libnss3.so" (Linux/WSL)
- Missing system dependencies
- Fix: Run
./install-deps.shin scripts directory - Manual install:
sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1
"Failed to launch the browser process"
- Check system dependencies installed (Linux/WSL)
- Verify Chrome downloaded:
ls ~/.cache/puppeteer - Try:
npm rebuildthennpm install
Chrome not found
- Puppeteer auto-downloads Chrome during
npm install - If failed, manually trigger:
npx puppeteer browsers install chrome
Script Issues
Element not found
- Get snapshot first to find correct selector:
node snapshot.js --url <url>
Script hangs
- Increase timeout:
--timeout 60000 - Change wait strategy:
--wait-until loador--wait-until domcontentloaded
Blank screenshot
- Wait for page load:
--wait-until networkidle2 - Increase timeout:
--timeout 30000
Permission denied on scripts
- Make executable:
chmod +x *.sh
Reference Documentation
Detailed guides available in ./references/:
- CDP Domains Reference - 47 Chrome DevTools Protocol domains
- Puppeteer Quick Reference - Complete Puppeteer API patterns
- Performance Analysis Guide - Core Web Vitals optimization
Advanced Usage
Custom Scripts
Create custom scripts using shared library:
import { getBrowser, getPage, closeBrowser, outputJSON } from './lib/browser.js';
// Your automation logicDirect CDP Access
const client = await page.createCDPSession();
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });See reference documentation for advanced patterns and complete API coverage.
External Resources
- Puppeteer Documentation
- Chrome DevTools Protocol
- Scripts README
Chrome DevTools Protocol (CDP) Domains Reference
Complete reference of CDP domains and their capabilities for browser automation and debugging.
Overview
CDP is organized into 47 domains, each providing specific browser capabilities. Domains are grouped by functionality:
- Core - Fundamental browser control
- DOM & Styling - Page structure and styling
- Network & Fetch - HTTP traffic management
- Page & Navigation - Page lifecycle control
- Storage & Data - Browser storage APIs
- Performance & Profiling - Metrics and analysis
- Emulation & Simulation - Device and network emulation
- Worker & Service - Background tasks
- Developer Tools - Debugging support
---
Core Domains
Runtime
Purpose: Execute JavaScript, manage objects, handle promises
Key Commands:
Runtime.evaluate(expression)- Execute JavaScriptRuntime.callFunctionOn(functionDeclaration, objectId)- Call function on objectRuntime.getProperties(objectId)- Get object propertiesRuntime.awaitPromise(promiseObjectId)- Wait for promise resolution
Key Events:
Runtime.consoleAPICalled- Console message loggedRuntime.exceptionThrown- Uncaught exception
Use Cases:
- Execute custom JavaScript
- Access page data
- Monitor console output
- Handle exceptions
---
Debugger
Purpose: JavaScript debugging, breakpoints, stack traces
Key Commands:
Debugger.enable()- Enable debuggerDebugger.setBreakpoint(location)- Set breakpointDebugger.pause()- Pause executionDebugger.resume()- Resume executionDebugger.stepOver/stepInto/stepOut()- Step through code
Key Events:
Debugger.paused- Execution pausedDebugger.resumed- Execution resumedDebugger.scriptParsed- Script loaded
Use Cases:
- Debug JavaScript errors
- Inspect call stacks
- Set conditional breakpoints
- Source map support
---
Console (Deprecated - Use Runtime/Log)
Purpose: Legacy console message access
Note: Use Runtime.consoleAPICalled event instead for new implementations.
---
DOM & Styling Domains
DOM
Purpose: Access and manipulate DOM tree
Key Commands:
DOM.getDocument()- Get root document nodeDOM.querySelector(nodeId, selector)- Query selectorDOM.querySelectorAll(nodeId, selector)- Query allDOM.getAttributes(nodeId)- Get element attributesDOM.setOuterHTML(nodeId, outerHTML)- Replace elementDOM.getBoxModel(nodeId)- Get element layout boxDOM.focus(nodeId)- Focus element
Key Events:
DOM.documentUpdated- Document changedDOM.setChildNodes- Child nodes updated
Use Cases:
- Navigate DOM tree
- Query elements
- Modify DOM structure
- Get element positions
---
CSS
Purpose: Inspect and modify CSS styles
Key Commands:
CSS.enable()- Enable CSS domainCSS.getComputedStyleForNode(nodeId)- Get computed stylesCSS.getInlineStylesForNode(nodeId)- Get inline stylesCSS.getMatchedStylesForNode(nodeId)- Get matched CSS rulesCSS.setStyleTexts(edits)- Modify styles
Key Events:
CSS.styleSheetAdded- Stylesheet addedCSS.styleSheetChanged- Stylesheet modified
Use Cases:
- Inspect element styles
- Debug CSS issues
- Modify styles dynamically
- Extract stylesheet data
---
Accessibility
Purpose: Access accessibility tree
Key Commands:
Accessibility.enable()- Enable accessibilityAccessibility.getFullAXTree()- Get complete AX treeAccessibility.getPartialAXTree(nodeId)- Get node subtreeAccessibility.queryAXTree(nodeId, role, name)- Query AX tree
Use Cases:
- Accessibility testing
- Screen reader simulation
- ARIA attribute inspection
- AX tree analysis
---
Network & Fetch Domains
Network
Purpose: Monitor and control HTTP traffic
Key Commands:
Network.enable()- Enable network trackingNetwork.setCacheDisabled(cacheDisabled)- Disable cacheNetwork.setExtraHTTPHeaders(headers)- Add custom headersNetwork.getCookies(urls)- Get cookiesNetwork.setCookie(name, value, domain)- Set cookieNetwork.getResponseBody(requestId)- Get response bodyNetwork.emulateNetworkConditions(offline, latency, downloadThroughput, uploadThroughput)- Throttle network
Key Events:
Network.requestWillBeSent- Request startingNetwork.responseReceived- Response receivedNetwork.loadingFinished- Request completedNetwork.loadingFailed- Request failed
Use Cases:
- Monitor API calls
- Intercept requests
- Analyze response data
- Simulate slow networks
- Manage cookies
---
Fetch
Purpose: Intercept and modify network requests
Key Commands:
Fetch.enable(patterns)- Enable request interceptionFetch.continueRequest(requestId, url, method, headers)- Continue/modify requestFetch.fulfillRequest(requestId, responseCode, headers, body)- Mock responseFetch.failRequest(requestId, errorReason)- Fail request
Key Events:
Fetch.requestPaused- Request intercepted
Use Cases:
- Mock API responses
- Block requests
- Modify request/response
- Test error scenarios
---
Page & Navigation Domains
Page
Purpose: Control page lifecycle and navigation
Key Commands:
Page.enable()- Enable page domainPage.navigate(url)- Navigate to URLPage.reload(ignoreCache)- Reload pagePage.goBack()/goForward()- Navigate historyPage.captureScreenshot(format, quality)- Take screenshotPage.printToPDF(landscape, displayHeaderFooter)- Generate PDFPage.getLayoutMetrics()- Get page dimensionsPage.createIsolatedWorld(frameId)- Create isolated contextPage.handleJavaScriptDialog(accept, promptText)- Handle alerts/confirms
Key Events:
Page.loadEventFired- Page loadedPage.domContentEventFired- DOM readyPage.frameNavigated- Frame navigatedPage.javascriptDialogOpening- Alert/confirm shown
Use Cases:
- Navigate pages
- Capture screenshots
- Generate PDFs
- Handle popups
- Monitor page lifecycle
---
Target
Purpose: Manage browser targets (tabs, workers, frames)
Key Commands:
Target.getTargets()- List all targetsTarget.createTarget(url)- Open new tabTarget.closeTarget(targetId)- Close tabTarget.attachToTarget(targetId)- Attach debuggerTarget.detachFromTarget(sessionId)- Detach debuggerTarget.setDiscoverTargets(discover)- Auto-discover targets
Key Events:
Target.targetCreated- New target createdTarget.targetDestroyed- Target closedTarget.targetInfoChanged- Target updated
Use Cases:
- Multi-tab automation
- Service worker debugging
- Frame inspection
- Extension debugging
---
Input
Purpose: Simulate user input
Key Commands:
Input.dispatchKeyEvent(type, key, code)- Keyboard inputInput.dispatchMouseEvent(type, x, y, button)- Mouse inputInput.dispatchTouchEvent(type, touchPoints)- Touch inputInput.synthesizePinchGesture(x, y, scaleFactor)- Pinch gestureInput.synthesizeScrollGesture(x, y, xDistance, yDistance)- Scroll
Use Cases:
- Simulate clicks
- Type text
- Drag and drop
- Touch gestures
- Scroll pages
---
Storage & Data Domains
Storage
Purpose: Manage browser storage
Key Commands:
Storage.getCookies(browserContextId)- Get cookiesStorage.setCookies(cookies)- Set cookiesStorage.clearCookies(browserContextId)- Clear cookiesStorage.clearDataForOrigin(origin, storageTypes)- Clear storageStorage.getUsageAndQuota(origin)- Get storage usage
Storage Types:
- appcache, cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage
Use Cases:
- Cookie management
- Clear browser data
- Inspect storage usage
- Test quota limits
---
DOMStorage
Purpose: Access localStorage/sessionStorage
Key Commands:
DOMStorage.enable()- Enable storage trackingDOMStorage.getDOMStorageItems(storageId)- Get itemsDOMStorage.setDOMStorageItem(storageId, key, value)- Set itemDOMStorage.removeDOMStorageItem(storageId, key)- Remove item
Key Events:
DOMStorage.domStorageItemsCleared- Storage clearedDOMStorage.domStorageItemAdded/Updated/Removed- Item changed
---
IndexedDB
Purpose: Query IndexedDB databases
Key Commands:
IndexedDB.requestDatabaseNames(securityOrigin)- List databasesIndexedDB.requestDatabase(securityOrigin, databaseName)- Get DB structureIndexedDB.requestData(securityOrigin, databaseName, objectStoreName)- Query data
Use Cases:
- Inspect IndexedDB data
- Debug database issues
- Extract stored data
---
CacheStorage
Purpose: Manage Cache API
Key Commands:
CacheStorage.requestCacheNames(securityOrigin)- List cachesCacheStorage.requestCachedResponses(cacheId, securityOrigin)- List cached responsesCacheStorage.deleteCache(cacheId)- Delete cache
Use Cases:
- Service worker cache inspection
- Offline functionality testing
---
Performance & Profiling Domains
Performance
Purpose: Collect performance metrics
Key Commands:
Performance.enable()- Enable performance trackingPerformance.disable()- Disable trackingPerformance.getMetrics()- Get current metrics
Metrics:
- Timestamp, Documents, Frames, JSEventListeners, Nodes, LayoutCount, RecalcStyleCount, LayoutDuration, RecalcStyleDuration, ScriptDuration, TaskDuration, JSHeapUsedSize, JSHeapTotalSize
Use Cases:
- Monitor page metrics
- Track memory usage
- Measure render times
---
PerformanceTimeline
Purpose: Access Performance Timeline API
Key Commands:
PerformanceTimeline.enable(eventTypes)- Subscribe to events
Event Types:
- mark, measure, navigation, resource, longtask, paint, layout-shift
Key Events:
PerformanceTimeline.timelineEventAdded- New performance entry
---
Tracing
Purpose: Record Chrome trace
Key Commands:
Tracing.start(categories, options)- Start recordingTracing.end()- Stop recordingTracing.requestMemoryDump()- Capture memory snapshot
Trace Categories:
- blink, cc, devtools, gpu, loading, navigation, rendering, v8, disabled-by-default-*
Key Events:
Tracing.dataCollected- Trace chunk receivedTracing.tracingComplete- Recording finished
Use Cases:
- Deep performance analysis
- Frame rendering profiling
- CPU flame graphs
- Memory profiling
---
Profiler
Purpose: CPU profiling
Key Commands:
Profiler.enable()- Enable profilerProfiler.start()- Start CPU profilingProfiler.stop()- Stop and get profile
Use Cases:
- Find CPU bottlenecks
- Optimize JavaScript
- Generate flame graphs
---
HeapProfiler (via Memory domain)
Purpose: Memory profiling
Key Commands:
Memory.getDOMCounters()- Get DOM object countsMemory.prepareForLeakDetection()- Prepare leak detectionMemory.forciblyPurgeJavaScriptMemory()- Force GCMemory.setPressureNotificationsSuppressed(suppressed)- Control memory warningsMemory.simulatePressureNotification(level)- Simulate memory pressure
Use Cases:
- Detect memory leaks
- Analyze heap snapshots
- Monitor object counts
---
Emulation & Simulation Domains
Emulation
Purpose: Emulate device conditions
Key Commands:
Emulation.setDeviceMetricsOverride(width, height, deviceScaleFactor, mobile)- Emulate deviceEmulation.setGeolocationOverride(latitude, longitude, accuracy)- Fake locationEmulation.setEmulatedMedia(media, features)- Emulate media typeEmulation.setTimezoneOverride(timezoneId)- Override timezoneEmulation.setLocaleOverride(locale)- Override languageEmulation.setUserAgentOverride(userAgent)- Change user agent
Use Cases:
- Mobile device testing
- Geolocation testing
- Print media emulation
- Timezone/locale testing
---
DeviceOrientation
Purpose: Simulate device orientation
Key Commands:
DeviceOrientation.setDeviceOrientationOverride(alpha, beta, gamma)- Set orientation
Use Cases:
- Test accelerometer features
- Orientation-dependent layouts
---
Worker & Service Domains
ServiceWorker
Purpose: Manage service workers
Key Commands:
ServiceWorker.enable()- Enable trackingServiceWorker.unregister(scopeURL)- Unregister workerServiceWorker.startWorker(scopeURL)- Start workerServiceWorker.stopWorker(versionId)- Stop workerServiceWorker.inspectWorker(versionId)- Debug worker
Key Events:
ServiceWorker.workerRegistrationUpdated- Registration changedServiceWorker.workerVersionUpdated- Version updated
---
WebAuthn
Purpose: Simulate WebAuthn/FIDO2
Key Commands:
WebAuthn.enable()- Enable virtual authenticatorsWebAuthn.addVirtualAuthenticator(options)- Add virtual deviceWebAuthn.removeVirtualAuthenticator(authenticatorId)- Remove deviceWebAuthn.addCredential(authenticatorId, credential)- Add credential
Use Cases:
- Test WebAuthn flows
- Simulate biometric auth
- Test security keys
---
Developer Tools Support
Inspector
Purpose: Protocol-level debugging
Key Events:
Inspector.detached- Debugger disconnectedInspector.targetCrashed- Target crashed
---
Log
Purpose: Collect browser logs
Key Commands:
Log.enable()- Enable log collectionLog.clear()- Clear logs
Key Events:
Log.entryAdded- New log entry
Use Cases:
- Collect console logs
- Monitor violations
- Track deprecations
---
DOMDebugger
Purpose: DOM-level debugging
Key Commands:
DOMDebugger.setDOMBreakpoint(nodeId, type)- Break on DOM changesDOMDebugger.setEventListenerBreakpoint(eventName)- Break on eventDOMDebugger.setXHRBreakpoint(url)- Break on XHR
Breakpoint Types:
- subtree-modified, attribute-modified, node-removed
---
DOMSnapshot
Purpose: Capture complete DOM snapshot
Key Commands:
DOMSnapshot.captureSnapshot(computedStyles)- Capture full DOM
Use Cases:
- Export page structure
- Offline analysis
- DOM diffing
---
Audits (Lighthouse Integration)
Purpose: Run automated audits
Key Commands:
Audits.enable()- Enable auditsAudits.getEncodingIssues()- Check encoding issues
---
LayerTree
Purpose: Inspect rendering layers
Key Commands:
LayerTree.enable()- Enable layer trackingLayerTree.compositingReasons(layerId)- Get why layer created
Key Events:
LayerTree.layerTreeDidChange- Layers changed
Use Cases:
- Debug rendering performance
- Identify layer creation
- Optimize compositing
---
Other Domains
Browser
Purpose: Browser-level control
Key Commands:
Browser.getVersion()- Get browser infoBrowser.getBrowserCommandLine()- Get launch argsBrowser.setPermission(permission, setting, origin)- Set permissionsBrowser.grantPermissions(permissions, origin)- Grant permissions
Permissions:
- geolocation, midi, notifications, push, camera, microphone, background-sync, sensors, accessibility-events, clipboard-read, clipboard-write, payment-handler
---
IO
Purpose: File I/O operations
Key Commands:
IO.read(handle, offset, size)- Read streamIO.close(handle)- Close stream
Use Cases:
- Read large response bodies
- Process binary data
---
Media
Purpose: Inspect media players
Key Commands:
Media.enable()- Track media players
Key Events:
Media.playerPropertiesChanged- Player state changedMedia.playerEventsAdded- Player events
---
BackgroundService
Purpose: Track background services
Key Commands:
BackgroundService.startObserving(service)- Track service
Services:
- backgroundFetch, backgroundSync, pushMessaging, notifications, paymentHandler, periodicBackgroundSync
---
Domain Dependencies
Some domains depend on others and must be enabled in order:
Runtime (no dependencies)
↓
DOM (depends on Runtime)
↓
CSS (depends on DOM)
Network (no dependencies)
Page (depends on Runtime)
↓
Target (depends on Page)
Debugger (depends on Runtime)Quick Command Reference
Most Common Commands
// Navigation
Page.navigate(url)
Page.reload()
// JavaScript Execution
Runtime.evaluate(expression)
// DOM Access
DOM.getDocument()
DOM.querySelector(nodeId, selector)
// Screenshots
Page.captureScreenshot(format, quality)
// Network Monitoring
Network.enable()
// Listen for Network.requestWillBeSent events
// Console Messages
// Listen for Runtime.consoleAPICalled events
// Cookies
Network.getCookies(urls)
Network.setCookie(...)
// Device Emulation
Emulation.setDeviceMetricsOverride(width, height, ...)
// Performance
Performance.getMetrics()
Tracing.start(categories)
Tracing.end()---
Best Practices
1. Enable domains before use: Always call .enable() for stateful domains 2. Handle events: Subscribe to events for real-time updates 3. Clean up: Disable domains when done to reduce overhead 4. Use sessions: Attach to specific targets for isolated debugging 5. Handle errors: Implement proper error handling for command failures 6. Version awareness: Check browser version for experimental API support
---
Additional Resources
- Protocol Viewer - Interactive domain browser
- Protocol JSON - Machine-readable specification
- Getting Started with CDP
- devtools-protocol NPM - TypeScript definitions
Performance Analysis Guide
Comprehensive guide to analyzing web performance using Chrome DevTools Protocol, Puppeteer, and chrome-devtools-mcp tools.
Table of Contents
- Core Web Vitals
- Performance Tracing
- Network Analysis
- JavaScript Performance
- Rendering Performance
- Memory Analysis
- Optimization Strategies
---
Core Web Vitals
Overview
Core Web Vitals are Google's standardized metrics for measuring user experience:
- LCP (Largest Contentful Paint) - Loading performance (< 2.5s good)
- FID (First Input Delay) - Interactivity (< 100ms good)
- CLS (Cumulative Layout Shift) - Visual stability (< 0.1 good)
Measuring with chrome-devtools-mcp
// Start performance trace
await useTool('performance_start_trace', {
categories: ['loading', 'rendering', 'scripting']
});
// Navigate to page
await useTool('navigate_page', {
url: 'https://example.com'
});
// Wait for complete load
await useTool('wait_for', {
waitUntil: 'networkidle'
});
// Stop trace and get data
await useTool('performance_stop_trace');
// Get AI-powered insights
const insights = await useTool('performance_analyze_insight');
// insights will include:
// - LCP timing
// - FID analysis
// - CLS score
// - Performance recommendationsMeasuring with Puppeteer
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Measure Core Web Vitals
await page.goto('https://example.com', {
waitUntil: 'networkidle2'
});
const vitals = await page.evaluate(() => {
return new Promise((resolve) => {
const vitals = {
LCP: null,
FID: null,
CLS: 0
};
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
vitals.LCP = entries[entries.length - 1].renderTime ||
entries[entries.length - 1].loadTime;
}).observe({ entryTypes: ['largest-contentful-paint'] });
// FID
new PerformanceObserver((list) => {
vitals.FID = list.getEntries()[0].processingStart -
list.getEntries()[0].startTime;
}).observe({ entryTypes: ['first-input'] });
// CLS
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
vitals.CLS += entry.value;
}
});
}).observe({ entryTypes: ['layout-shift'] });
// Wait 5 seconds for metrics
setTimeout(() => resolve(vitals), 5000);
});
});
console.log('Core Web Vitals:', vitals);Other Important Metrics
TTFB (Time to First Byte)
const ttfb = await page.evaluate(() => {
const [navigationEntry] = performance.getEntriesByType('navigation');
return navigationEntry.responseStart - navigationEntry.requestStart;
});FCP (First Contentful Paint)
const fcp = await page.evaluate(() => {
const paintEntries = performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
return fcpEntry ? fcpEntry.startTime : null;
});TTI (Time to Interactive)
// Requires lighthouse or manual calculation
const tti = await page.evaluate(() => {
// Complex calculation based on network idle and long tasks
// Best to use Lighthouse for accurate TTI
});---
Performance Tracing
Chrome Trace Categories
Loading:
- Page load events
- Resource loading
- Parser activity
Rendering:
- Layout calculations
- Paint operations
- Compositing
Scripting:
- JavaScript execution
- V8 compilation
- Garbage collection
Network:
- HTTP requests
- WebSocket traffic
- Resource fetching
Input:
- User input processing
- Touch/scroll events
GPU:
- GPU operations
- Compositing work
Record Performance Trace
Using chrome-devtools-mcp:
// Start trace with specific categories
await useTool('performance_start_trace', {
categories: ['loading', 'rendering', 'scripting', 'network']
});
// Perform actions
await useTool('navigate_page', { url: 'https://example.com' });
await useTool('wait_for', { waitUntil: 'networkidle' });
// Optional: Interact with page
await useTool('click', { uid: 'button-uid' });
// Stop trace
const traceData = await useTool('performance_stop_trace');
// Analyze trace
const insights = await useTool('performance_analyze_insight');Using Puppeteer:
// Start tracing
await page.tracing.start({
path: 'trace.json',
categories: [
'devtools.timeline',
'disabled-by-default-devtools.timeline',
'disabled-by-default-v8.cpu_profiler'
]
});
// Navigate
await page.goto('https://example.com', {
waitUntil: 'networkidle2'
});
// Stop tracing
await page.tracing.stop();
// Analyze in Chrome DevTools (chrome://tracing)Analyze Trace Data
Key Metrics from Trace:
1. Main Thread Activity
- JavaScript execution time
- Layout/reflow time
- Paint time
- Long tasks (> 50ms)
2. Network Waterfall
- Request start times
- DNS lookup
- Connection time
- Download time
3. Rendering Pipeline
- DOM construction
- Style calculation
- Layout
- Paint
- Composite
Common Issues to Look For:
- Long tasks blocking main thread
- Excessive JavaScript execution
- Layout thrashing
- Unnecessary repaints
- Slow network requests
- Large bundle sizes
---
Network Analysis
Monitor Network Requests
Using chrome-devtools-mcp:
// Navigate to page
await useTool('navigate_page', { url: 'https://example.com' });
// Wait for all requests
await useTool('wait_for', { waitUntil: 'networkidle' });
// List all requests
const requests = await useTool('list_network_requests', {
resourceTypes: ['Document', 'Script', 'Stylesheet', 'Image', 'XHR', 'Fetch'],
pageSize: 100
});
// Analyze specific request
for (const req of requests.requests) {
const details = await useTool('get_network_request', {
requestId: req.id
});
console.log({
url: details.url,
method: details.method,
status: details.status,
size: details.encodedDataLength,
time: details.timing.receiveHeadersEnd - details.timing.requestTime,
cached: details.fromCache
});
}Using Puppeteer:
const requests = [];
// Capture all requests
page.on('request', (request) => {
requests.push({
url: request.url(),
method: request.method(),
resourceType: request.resourceType(),
headers: request.headers()
});
});
// Capture responses
page.on('response', (response) => {
const request = response.request();
console.log({
url: response.url(),
status: response.status(),
size: response.headers()['content-length'],
cached: response.fromCache(),
timing: response.timing()
});
});
await page.goto('https://example.com');Network Performance Metrics
Calculate Total Page Weight:
let totalBytes = 0;
let resourceCounts = {};
page.on('response', async (response) => {
const type = response.request().resourceType();
const buffer = await response.buffer();
totalBytes += buffer.length;
resourceCounts[type] = (resourceCounts[type] || 0) + 1;
});
await page.goto('https://example.com');
console.log('Total size:', (totalBytes / 1024 / 1024).toFixed(2), 'MB');
console.log('Resources:', resourceCounts);Identify Slow Requests:
page.on('response', (response) => {
const timing = response.timing();
const totalTime = timing.receiveHeadersEnd - timing.requestTime;
if (totalTime > 1000) { // Slower than 1 second
console.log('Slow request:', {
url: response.url(),
time: totalTime.toFixed(2) + 'ms',
size: response.headers()['content-length']
});
}
});Network Throttling
Simulate Slow Connection:
// Using chrome-devtools-mcp
await useTool('emulate_network', {
throttlingOption: 'Slow 3G' // or 'Fast 3G', 'Slow 4G'
});
// Using Puppeteer
const client = await page.createCDPSession();
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400 * 1024 / 8, // 400 Kbps
uploadThroughput: 400 * 1024 / 8,
latency: 2000 // 2000ms RTT
});---
JavaScript Performance
Identify Long Tasks
Using Performance Observer:
await page.evaluate(() => {
return new Promise((resolve) => {
const longTasks = [];
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
longTasks.push({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
});
});
observer.observe({ entryTypes: ['longtask'] });
// Collect for 10 seconds
setTimeout(() => {
observer.disconnect();
resolve(longTasks);
}, 10000);
});
});CPU Profiling
Using Puppeteer:
// Start CPU profiling
const client = await page.createCDPSession();
await client.send('Profiler.enable');
await client.send('Profiler.start');
// Navigate and interact
await page.goto('https://example.com');
await page.click('.button');
// Stop profiling
const { profile } = await client.send('Profiler.stop');
// Analyze profile (flame graph data)
// Import into Chrome DevTools for visualizationJavaScript Coverage
Identify Unused Code:
// Start coverage
await Promise.all([
page.coverage.startJSCoverage(),
page.coverage.startCSSCoverage()
]);
// Navigate
await page.goto('https://example.com');
// Stop coverage
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage()
]);
// Calculate unused bytes
function calculateUnusedBytes(coverage) {
let usedBytes = 0;
let totalBytes = 0;
for (const entry of coverage) {
totalBytes += entry.text.length;
for (const range of entry.ranges) {
usedBytes += range.end - range.start - 1;
}
}
return {
usedBytes,
totalBytes,
unusedBytes: totalBytes - usedBytes,
unusedPercentage: ((totalBytes - usedBytes) / totalBytes * 100).toFixed(2)
};
}
console.log('JS Coverage:', calculateUnusedBytes(jsCoverage));
console.log('CSS Coverage:', calculateUnusedBytes(cssCoverage));Bundle Size Analysis
Analyze JavaScript Bundles:
page.on('response', async (response) => {
const url = response.url();
const type = response.request().resourceType();
if (type === 'script') {
const buffer = await response.buffer();
const size = buffer.length;
console.log({
url: url.split('/').pop(),
size: (size / 1024).toFixed(2) + ' KB',
gzipped: response.headers()['content-encoding'] === 'gzip'
});
}
});---
Rendering Performance
Layout Thrashing Detection
Monitor Layout Recalculations:
// Using Performance Observer
await page.evaluate(() => {
return new Promise((resolve) => {
const measurements = [];
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'measure' &&
entry.name.includes('layout')) {
measurements.push({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
}
});
});
observer.observe({ entryTypes: ['measure'] });
setTimeout(() => {
observer.disconnect();
resolve(measurements);
}, 5000);
});
});Paint and Composite Metrics
Get Paint Metrics:
const paintMetrics = await page.evaluate(() => {
const paints = performance.getEntriesByType('paint');
return {
firstPaint: paints.find(p => p.name === 'first-paint')?.startTime,
firstContentfulPaint: paints.find(p => p.name === 'first-contentful-paint')?.startTime
};
});Frame Rate Analysis
Monitor FPS:
await page.evaluate(() => {
return new Promise((resolve) => {
let frames = 0;
let lastTime = performance.now();
function countFrames() {
frames++;
requestAnimationFrame(countFrames);
}
countFrames();
setTimeout(() => {
const now = performance.now();
const elapsed = (now - lastTime) / 1000;
const fps = frames / elapsed;
resolve(fps);
}, 5000);
});
});Layout Shifts (CLS)
Track Individual Shifts:
await page.evaluate(() => {
return new Promise((resolve) => {
const shifts = [];
let totalCLS = 0;
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
totalCLS += entry.value;
shifts.push({
value: entry.value,
time: entry.startTime,
elements: entry.sources?.map(s => s.node)
});
}
});
});
observer.observe({ entryTypes: ['layout-shift'] });
setTimeout(() => {
observer.disconnect();
resolve({ totalCLS, shifts });
}, 10000);
});
});---
Memory Analysis
Memory Metrics
Get Memory Usage:
// Using chrome-devtools-mcp
await useTool('evaluate_script', {
expression: `
({
usedJSHeapSize: performance.memory?.usedJSHeapSize,
totalJSHeapSize: performance.memory?.totalJSHeapSize,
jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit
})
`,
returnByValue: true
});
// Using Puppeteer
const metrics = await page.metrics();
console.log({
jsHeapUsed: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2) + ' MB',
jsHeapTotal: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2) + ' MB',
domNodes: metrics.Nodes,
documents: metrics.Documents,
jsEventListeners: metrics.JSEventListeners
});Memory Leak Detection
Monitor Memory Over Time:
async function detectMemoryLeak(page, duration = 30000) {
const samples = [];
const interval = 1000; // Sample every second
const samples_count = duration / interval;
for (let i = 0; i < samples_count; i++) {
const metrics = await page.metrics();
samples.push({
time: i,
heapUsed: metrics.JSHeapUsedSize
});
await page.waitForTimeout(interval);
}
// Analyze trend
const firstSample = samples[0].heapUsed;
const lastSample = samples[samples.length - 1].heapUsed;
const increase = ((lastSample - firstSample) / firstSample * 100).toFixed(2);
return {
samples,
memoryIncrease: increase + '%',
possibleLeak: increase > 50 // > 50% increase indicates possible leak
};
}
const leakAnalysis = await detectMemoryLeak(page, 30000);
console.log('Memory Analysis:', leakAnalysis);Heap Snapshot
Capture Heap Snapshot:
const client = await page.createCDPSession();
// Take snapshot
await client.send('HeapProfiler.enable');
const { result } = await client.send('HeapProfiler.takeHeapSnapshot');
// Snapshot is streamed in chunks
// Save to file or analyze programmatically---
Optimization Strategies
Image Optimization
Detect Unoptimized Images:
const images = await page.evaluate(() => {
const images = Array.from(document.querySelectorAll('img'));
return images.map(img => ({
src: img.src,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
displayWidth: img.width,
displayHeight: img.height,
oversized: img.naturalWidth > img.width * 1.5 ||
img.naturalHeight > img.height * 1.5
}));
});
const oversizedImages = images.filter(img => img.oversized);
console.log('Oversized images:', oversizedImages);Font Loading
Detect Render-Blocking Fonts:
const fonts = await page.evaluate(() => {
return Array.from(document.fonts).map(font => ({
family: font.family,
weight: font.weight,
style: font.style,
status: font.status,
loaded: font.status === 'loaded'
}));
});
console.log('Fonts:', fonts);Third-Party Scripts
Measure Third-Party Impact:
const thirdPartyDomains = ['googletagmanager.com', 'facebook.net', 'doubleclick.net'];
page.on('response', async (response) => {
const url = response.url();
const isThirdParty = thirdPartyDomains.some(domain => url.includes(domain));
if (isThirdParty) {
const buffer = await response.buffer();
console.log({
url: url,
size: (buffer.length / 1024).toFixed(2) + ' KB',
type: response.request().resourceType()
});
}
});Critical Rendering Path
Identify Render-Blocking Resources:
await page.goto('https://example.com');
const renderBlockingResources = await page.evaluate(() => {
const resources = performance.getEntriesByType('resource');
return resources.filter(resource => {
return (resource.initiatorType === 'link' &&
resource.name.includes('.css')) ||
(resource.initiatorType === 'script' &&
!resource.name.includes('async'));
}).map(r => ({
url: r.name,
duration: r.duration,
startTime: r.startTime
}));
});
console.log('Render-blocking resources:', renderBlockingResources);Lighthouse Integration
Run Lighthouse Audit:
import lighthouse from 'lighthouse';
import { launch } from 'chrome-launcher';
// Launch Chrome
const chrome = await launch({ chromeFlags: ['--headless'] });
// Run Lighthouse
const { lhr } = await lighthouse('https://example.com', {
port: chrome.port,
onlyCategories: ['performance']
});
// Get scores
console.log({
performanceScore: lhr.categories.performance.score * 100,
metrics: {
FCP: lhr.audits['first-contentful-paint'].displayValue,
LCP: lhr.audits['largest-contentful-paint'].displayValue,
TBT: lhr.audits['total-blocking-time'].displayValue,
CLS: lhr.audits['cumulative-layout-shift'].displayValue,
SI: lhr.audits['speed-index'].displayValue
},
opportunities: lhr.audits['opportunities']
});
await chrome.kill();---
Performance Budgets
Set Performance Budgets
const budgets = {
// Core Web Vitals
LCP: 2500, // ms
FID: 100, // ms
CLS: 0.1, // score
// Other metrics
FCP: 1800, // ms
TTI: 3800, // ms
TBT: 300, // ms
// Resource budgets
totalPageSize: 2 * 1024 * 1024, // 2 MB
jsSize: 500 * 1024, // 500 KB
cssSize: 100 * 1024, // 100 KB
imageSize: 1 * 1024 * 1024, // 1 MB
// Request counts
totalRequests: 50,
jsRequests: 10,
cssRequests: 5
};
async function checkBudgets(page, budgets) {
// Measure actual values
const vitals = await measureCoreWebVitals(page);
const resources = await analyzeResources(page);
// Compare against budgets
const violations = [];
if (vitals.LCP > budgets.LCP) {
violations.push(`LCP: ${vitals.LCP}ms exceeds budget of ${budgets.LCP}ms`);
}
if (resources.totalSize > budgets.totalPageSize) {
violations.push(`Page size: ${resources.totalSize} exceeds budget of ${budgets.totalPageSize}`);
}
// ... check other budgets
return {
passed: violations.length === 0,
violations
};
}---
Automated Performance Testing
CI/CD Integration
// performance-test.js
import puppeteer from 'puppeteer';
async function performanceTest(url) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Measure metrics
await page.goto(url, { waitUntil: 'networkidle2' });
const metrics = await page.metrics();
const vitals = await measureCoreWebVitals(page);
await browser.close();
// Check against thresholds
const thresholds = {
LCP: 2500,
FID: 100,
CLS: 0.1,
jsHeapSize: 50 * 1024 * 1024 // 50 MB
};
const failed = [];
if (vitals.LCP > thresholds.LCP) failed.push('LCP');
if (vitals.FID > thresholds.FID) failed.push('FID');
if (vitals.CLS > thresholds.CLS) failed.push('CLS');
if (metrics.JSHeapUsedSize > thresholds.jsHeapSize) failed.push('Memory');
if (failed.length > 0) {
console.error('Performance test failed:', failed);
process.exit(1);
}
console.log('Performance test passed');
}
performanceTest(process.env.TEST_URL);---
Best Practices
Performance Testing Checklist
1. Measure Multiple Times
- Run tests 3-5 times
- Use median values
- Account for variance
2. Test Different Conditions
- Fast 3G
- Slow 3G
- Offline
- CPU throttling
3. Test Different Devices
- Mobile (low-end)
- Mobile (high-end)
- Desktop
- Tablet
4. Monitor Over Time
- Track metrics in CI/CD
- Set up alerts for regressions
- Create performance dashboards
5. Focus on User Experience
- Prioritize Core Web Vitals
- Test real user journeys
- Consider perceived performance
6. Optimize Critical Path
- Minimize render-blocking resources
- Defer non-critical JavaScript
- Optimize font loading
- Lazy load images
---
Resources
Puppeteer Quick Reference
Complete guide to browser automation with Puppeteer - a high-level API over Chrome DevTools Protocol.
Table of Contents
- Setup
- Browser & Page Management
- Navigation
- Element Interaction
- JavaScript Execution
- Screenshots & PDFs
- Network Interception
- Device Emulation
- Performance
- Common Patterns
---
Setup
Installation
# Install Puppeteer
npm install puppeteer
# Install core only (bring your own Chrome)
npm install puppeteer-coreBasic Usage
import puppeteer from 'puppeteer';
// Launch browser
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
// Open page
const page = await browser.newPage();
// Navigate
await page.goto('https://example.com');
// Do work...
// Cleanup
await browser.close();---
Browser & Page Management
Launch Browser
const browser = await puppeteer.launch({
// Visibility
headless: false, // Show browser UI
headless: 'new', // New headless mode (Chrome 112+)
// Chrome location
executablePath: '/path/to/chrome',
channel: 'chrome', // or 'chrome-canary', 'chrome-beta'
// Browser context
userDataDir: './user-data', // Persistent profile
// Window size
defaultViewport: {
width: 1920,
height: 1080,
deviceScaleFactor: 1,
isMobile: false
},
// Advanced options
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-features=IsolateOrigins',
'--disable-site-isolation-trials',
'--start-maximized'
],
// Debugging
devtools: true, // Open DevTools automatically
slowMo: 250, // Slow down by 250ms per action
// Network
proxy: {
server: 'http://proxy.com:8080'
}
});Connect to Running Browser
// Launch Chrome with debugging
// google-chrome --remote-debugging-port=9222
const browser = await puppeteer.connect({
browserURL: 'http://localhost:9222',
// or browserWSEndpoint: 'ws://localhost:9222/devtools/browser/...'
});Page Management
// Create new page
const page = await browser.newPage();
// Get all pages
const pages = await browser.pages();
// Close page
await page.close();
// Multiple pages
const page1 = await browser.newPage();
const page2 = await browser.newPage();
// Switch between pages
await page1.bringToFront();Browser Context (Incognito)
// Create isolated context
const context = await browser.createBrowserContext();
const page = await context.newPage();
// Cleanup context
await context.close();---
Navigation
Basic Navigation
// Navigate to URL
await page.goto('https://example.com');
// Navigate with options
await page.goto('https://example.com', {
waitUntil: 'networkidle2', // or 'load', 'domcontentloaded', 'networkidle0'
timeout: 30000 // Max wait time (ms)
});
// Reload page
await page.reload({ waitUntil: 'networkidle2' });
// Navigation history
await page.goBack();
await page.goForward();
// Wait for navigation
await page.waitForNavigation({
waitUntil: 'networkidle2'
});Wait Until Options
load- Wait for load eventdomcontentloaded- Wait for DOMContentLoaded eventnetworkidle0- Wait until no network connections for 500msnetworkidle2- Wait until max 2 network connections for 500ms
---
Element Interaction
Selectors
// CSS selectors
await page.$('#id');
await page.$('.class');
await page.$('div > p');
// XPath
await page.$x('//button[text()="Submit"]');
// Get all matching elements
await page.$$('.item');
await page.$$x('//div[@class="item"]');Click Elements
// Click by selector
await page.click('.button');
// Click with options
await page.click('.button', {
button: 'left', // or 'right', 'middle'
clickCount: 1, // 2 for double-click
delay: 100 // Delay between mousedown and mouseup
});
// ElementHandle click
const button = await page.$('.button');
await button.click();Type Text
// Type into input
await page.type('#search', 'query text');
// Type with delay
await page.type('#search', 'slow typing', { delay: 100 });
// Clear and type
await page.$eval('#search', el => el.value = '');
await page.type('#search', 'new text');Form Interaction
// Fill input
await page.type('#username', 'john@example.com');
await page.type('#password', 'secret123');
// Select dropdown option
await page.select('#country', 'US'); // By value
await page.select('#country', 'USA', 'UK'); // Multiple
// Check/uncheck checkbox
await page.click('input[type="checkbox"]');
// Choose radio button
await page.click('input[value="option2"]');
// Upload file
const input = await page.$('input[type="file"]');
await input.uploadFile('/path/to/file.pdf');
// Submit form
await page.click('button[type="submit"]');
await page.waitForNavigation();Hover & Focus
// Hover over element
await page.hover('.menu-item');
// Focus element
await page.focus('#input');
// Blur
await page.$eval('#input', el => el.blur());Drag & Drop
const source = await page.$('.draggable');
const target = await page.$('.drop-zone');
await source.drag(target);
await source.drop(target);---
JavaScript Execution
Evaluate in Page Context
// Execute JavaScript
const title = await page.evaluate(() => document.title);
// With arguments
const text = await page.evaluate(
(selector) => document.querySelector(selector).textContent,
'.heading'
);
// Return complex data
const data = await page.evaluate(() => ({
title: document.title,
url: location.href,
cookies: document.cookie
}));
// With ElementHandle
const element = await page.$('.button');
const text = await page.evaluate(el => el.textContent, element);Query & Modify DOM
// Get element property
const value = await page.$eval('#input', el => el.value);
// Get multiple elements
const items = await page.$$eval('.item', elements =>
elements.map(el => el.textContent)
);
// Modify element
await page.$eval('#input', (el, value) => {
el.value = value;
}, 'new value');
// Add class
await page.$eval('.element', el => el.classList.add('active'));Expose Functions
// Expose Node.js function to page
await page.exposeFunction('md5', (text) =>
crypto.createHash('md5').update(text).digest('hex')
);
// Call from page context
const hash = await page.evaluate(async () => {
return await window.md5('hello world');
});---
Screenshots & PDFs
Screenshots
// Full page screenshot
await page.screenshot({
path: 'screenshot.png',
fullPage: true
});
// Viewport screenshot
await page.screenshot({
path: 'viewport.png',
fullPage: false
});
// Element screenshot
const element = await page.$('.chart');
await element.screenshot({
path: 'chart.png'
});
// Screenshot options
await page.screenshot({
path: 'page.png',
type: 'png', // or 'jpeg', 'webp'
quality: 80, // JPEG quality (0-100)
clip: { // Crop region
x: 0,
y: 0,
width: 500,
height: 500
},
omitBackground: true // Transparent background
});
// Screenshot to buffer
const buffer = await page.screenshot();PDF Generation
// Generate PDF
await page.pdf({
path: 'page.pdf',
format: 'A4', // or 'Letter', 'Legal', etc.
printBackground: true,
margin: {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm'
}
});
// Custom page size
await page.pdf({
path: 'custom.pdf',
width: '8.5in',
height: '11in',
landscape: true
});
// Header and footer
await page.pdf({
path: 'report.pdf',
displayHeaderFooter: true,
headerTemplate: '<div style="font-size:10px;">Header</div>',
footerTemplate: '<div style="font-size:10px;">Page <span class="pageNumber"></span></div>'
});---
Network Interception
Request Interception
// Enable request interception
await page.setRequestInterception(true);
// Intercept requests
page.on('request', (request) => {
// Block specific resource types
if (request.resourceType() === 'image') {
request.abort();
}
// Block URLs
else if (request.url().includes('ads')) {
request.abort();
}
// Modify request
else if (request.url().includes('api')) {
request.continue({
headers: {
...request.headers(),
'Authorization': 'Bearer token'
}
});
}
// Continue normally
else {
request.continue();
}
});Mock Responses
await page.setRequestInterception(true);
page.on('request', (request) => {
if (request.url().includes('/api/user')) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
name: 'Mock User'
})
});
} else {
request.continue();
}
});Monitor Network
// Track requests
page.on('request', (request) => {
console.log('Request:', request.method(), request.url());
});
// Track responses
page.on('response', (response) => {
console.log('Response:', response.status(), response.url());
});
// Track failed requests
page.on('requestfailed', (request) => {
console.log('Failed:', request.failure().errorText, request.url());
});
// Get response body
page.on('response', async (response) => {
if (response.url().includes('/api/data')) {
const json = await response.json();
console.log('API Data:', json);
}
});---
Device Emulation
Predefined Devices
import { devices } from 'puppeteer';
// Emulate iPhone
const iPhone = devices['iPhone 13 Pro'];
await page.emulate(iPhone);
// Common devices
const iPad = devices['iPad Pro'];
const pixel = devices['Pixel 5'];
const galaxy = devices['Galaxy S9+'];
// Navigate after emulation
await page.goto('https://example.com');Custom Device
await page.emulate({
viewport: {
width: 375,
height: 812,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false
},
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)...'
});Viewport Only
await page.setViewport({
width: 1920,
height: 1080,
deviceScaleFactor: 1
});Geolocation
// Set geolocation
await page.setGeolocation({
latitude: 37.7749,
longitude: -122.4194,
accuracy: 100
});
// Grant permissions
const context = browser.defaultBrowserContext();
await context.overridePermissions('https://example.com', ['geolocation']);Timezone & Locale
// Set timezone
await page.emulateTimezone('America/New_York');
// Set locale
await page.emulateMediaType('screen');
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'language', {
get: () => 'en-US'
});
});---
Performance
CPU & Network Throttling
// CPU throttling
const client = await page.createCDPSession();
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
// Network throttling
await page.emulateNetworkConditions({
offline: false,
downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
uploadThroughput: 750 * 1024 / 8, // 750 Kbps
latency: 40 // 40ms RTT
});
// Predefined profiles
await page.emulateNetworkConditions(
puppeteer.networkConditions['Fast 3G']
);
// Disable throttling
await page.emulateNetworkConditions({
offline: false,
downloadThroughput: -1,
uploadThroughput: -1,
latency: 0
});Performance Metrics
// Get metrics
const metrics = await page.metrics();
console.log(metrics);
// {
// Timestamp, Documents, Frames, JSEventListeners,
// Nodes, LayoutCount, RecalcStyleCount,
// LayoutDuration, RecalcStyleDuration,
// ScriptDuration, TaskDuration,
// JSHeapUsedSize, JSHeapTotalSize
// }Performance Tracing
// Start tracing
await page.tracing.start({
path: 'trace.json',
categories: [
'devtools.timeline',
'disabled-by-default-devtools.timeline'
]
});
// Navigate
await page.goto('https://example.com');
// Stop tracing
await page.tracing.stop();
// Analyze trace in chrome://tracingCoverage (Code Usage)
// Start JS coverage
await page.coverage.startJSCoverage();
// Start CSS coverage
await page.coverage.startCSSCoverage();
// Navigate
await page.goto('https://example.com');
// Stop and get coverage
const jsCoverage = await page.coverage.stopJSCoverage();
const cssCoverage = await page.coverage.stopCSSCoverage();
// Calculate unused bytes
let totalBytes = 0;
let usedBytes = 0;
for (const entry of [...jsCoverage, ...cssCoverage]) {
totalBytes += entry.text.length;
for (const range of entry.ranges) {
usedBytes += range.end - range.start - 1;
}
}
console.log(`Used: ${usedBytes / totalBytes * 100}%`);---
Common Patterns
Wait for Elements
// Wait for selector
await page.waitForSelector('.element', {
visible: true,
timeout: 5000
});
// Wait for XPath
await page.waitForXPath('//button[text()="Submit"]');
// Wait for function
await page.waitForFunction(
() => document.querySelector('.loading') === null,
{ timeout: 10000 }
);
// Wait for timeout
await page.waitForTimeout(2000);Handle Dialogs
// Alert, confirm, prompt
page.on('dialog', async (dialog) => {
console.log(dialog.type(), dialog.message());
// Accept
await dialog.accept();
// or reject
// await dialog.dismiss();
// or provide input for prompt
// await dialog.accept('input text');
});Handle Downloads
// Set download path
const client = await page.createCDPSession();
await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: '/path/to/downloads'
});
// Trigger download
await page.click('a[download]');Multiple Pages (Tabs)
// Listen for new pages
browser.on('targetcreated', async (target) => {
if (target.type() === 'page') {
const newPage = await target.page();
console.log('New page opened:', newPage.url());
}
});
// Click link that opens new tab
const [newPage] = await Promise.all([
new Promise(resolve => browser.once('targetcreated', target => resolve(target.page()))),
page.click('a[target="_blank"]')
]);
console.log('New page URL:', newPage.url());Frames (iframes)
// Get all frames
const frames = page.frames();
// Find frame by name
const frame = page.frames().find(f => f.name() === 'myframe');
// Find frame by URL
const frame = page.frames().find(f => f.url().includes('example.com'));
// Main frame
const mainFrame = page.mainFrame();
// Interact with frame
await frame.click('.button');
await frame.type('#input', 'text');Infinite Scroll
async function autoScroll(page) {
await page.evaluate(async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
}
await autoScroll(page);Cookies
// Get cookies
const cookies = await page.cookies();
// Set cookies
await page.setCookie({
name: 'session',
value: 'abc123',
domain: 'example.com',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Strict'
});
// Delete cookies
await page.deleteCookie({ name: 'session' });Local Storage
// Set localStorage
await page.evaluate(() => {
localStorage.setItem('key', 'value');
});
// Get localStorage
const value = await page.evaluate(() => {
return localStorage.getItem('key');
});
// Clear localStorage
await page.evaluate(() => localStorage.clear());Error Handling
try {
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 30000
});
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Page load timeout');
} else {
console.error('Navigation failed:', error);
}
// Take screenshot on error
await page.screenshot({ path: 'error.png' });
}Stealth Mode (Avoid Detection)
// Hide automation indicators
await page.evaluateOnNewDocument(() => {
// Override navigator.webdriver
Object.defineProperty(navigator, 'webdriver', {
get: () => false
});
// Mock chrome object
window.chrome = {
runtime: {}
};
// Mock permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: 'granted' }) :
originalQuery(parameters)
);
});
// Set realistic user agent
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);---
Debugging Tips
Take Screenshots on Error
page.on('pageerror', async (error) => {
console.error('Page error:', error);
await page.screenshot({ path: `error-${Date.now()}.png` });
});Console Logging
// Forward console to Node
page.on('console', (msg) => {
console.log('PAGE LOG:', msg.text());
});Slow Down Execution
const browser = await puppeteer.launch({
slowMo: 250 // 250ms delay between actions
});Keep Browser Open
const browser = await puppeteer.launch({
headless: false,
devtools: true
});
// Prevent auto-close
await page.evaluate(() => debugger);---
Best Practices
1. Always close browser: Use try/finally or process cleanup 2. Wait appropriately: Use waitForSelector, not setTimeout 3. Handle errors: Wrap navigation in try/catch 4. Optimize selectors: Use specific selectors for reliability 5. Avoid race conditions: Wait for navigation after clicks 6. Reuse pages: Don't create new pages unnecessarily 7. Set timeouts: Always specify reasonable timeouts 8. Clean up: Close unused pages and contexts
---
Resources
node_modules#!/usr/bin/env node
/**
* Click an element
* Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function click() {
const args = parseArgs(process.argv.slice(2));
if (!args.selector) {
outputError(new Error('--selector is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
// Wait for element
await page.waitForSelector(args.selector, {
visible: true,
timeout: parseInt(args.timeout || '5000')
});
// Click element
await page.click(args.selector);
// Wait for optional selector after click
if (args['wait-for']) {
await page.waitForSelector(args['wait-for'], {
timeout: parseInt(args.timeout || '5000')
});
} else {
// Wait for navigation or timeout
try {
await page.waitForNavigation({
waitUntil: 'networkidle2',
timeout: 2000
});
} catch (e) {
// Ignore timeout - no navigation occurred
}
}
outputJSON({
success: true,
url: page.url(),
title: await page.title()
});
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
click();
#!/usr/bin/env node
/**
* Monitor console messages
* Usage: node console.js --url https://example.com [--types error,warn] [--duration 5000]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function monitorConsole() {
const args = parseArgs(process.argv.slice(2));
if (!args.url) {
outputError(new Error('--url is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
const messages = [];
const filterTypes = args.types ? args.types.split(',') : null;
// Listen for console messages
page.on('console', (msg) => {
const type = msg.type();
if (!filterTypes || filterTypes.includes(type)) {
messages.push({
type: type,
text: msg.text(),
location: msg.location(),
timestamp: Date.now()
});
}
});
// Listen for page errors
page.on('pageerror', (error) => {
messages.push({
type: 'pageerror',
text: error.message,
stack: error.stack,
timestamp: Date.now()
});
});
// Navigate
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
// Wait for additional time if specified
if (args.duration) {
await new Promise(resolve => setTimeout(resolve, parseInt(args.duration)));
}
outputJSON({
success: true,
url: page.url(),
messageCount: messages.length,
messages: messages
});
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
monitorConsole();
#!/usr/bin/env node
/**
* Execute JavaScript in page context
* Usage: node evaluate.js --script "document.title" [--url https://example.com]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function evaluate() {
const args = parseArgs(process.argv.slice(2));
if (!args.script) {
outputError(new Error('--script is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
const result = await page.evaluate((script) => {
// eslint-disable-next-line no-eval
return eval(script);
}, args.script);
outputJSON({
success: true,
result: result,
url: page.url()
});
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
evaluate();
#!/usr/bin/env node
/**
* Fill form fields
* Usage: node fill.js --selector "#input" --value "text" [--url https://example.com]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function fill() {
const args = parseArgs(process.argv.slice(2));
if (!args.selector) {
outputError(new Error('--selector is required'));
return;
}
if (!args.value) {
outputError(new Error('--value is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
// Wait for element
await page.waitForSelector(args.selector, {
visible: true,
timeout: parseInt(args.timeout || '5000')
});
// Clear existing value if specified
if (args.clear === 'true') {
await page.$eval(args.selector, el => el.value = '');
}
// Type into element
await page.type(args.selector, args.value, {
delay: parseInt(args.delay || '0')
});
outputJSON({
success: true,
selector: args.selector,
value: args.value,
url: page.url()
});
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
fill();
#!/bin/bash
# System dependencies installation script for Chrome DevTools Agent Skill
# This script installs required system libraries for running Chrome/Chromium
set -e
echo "🚀 Installing system dependencies for Chrome/Chromium..."
echo ""
# Detect OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
else
echo "❌ Cannot detect OS. This script supports Debian/Ubuntu-based systems."
exit 1
fi
# Check if running as root
if [ "$EUID" -ne 0 ]; then
SUDO="sudo"
echo "⚠️ This script requires root privileges to install system packages."
echo " You may be prompted for your password."
echo ""
else
SUDO=""
fi
# Install dependencies based on OS
case $OS in
ubuntu|debian|pop)
echo "Detected: $PRETTY_NAME"
echo "Installing dependencies with apt..."
echo ""
$SUDO apt-get update
# Install Chrome dependencies
$SUDO apt-get install -y \
ca-certificates \
fonts-liberation \
libasound2t64 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
wget \
xdg-utils
echo ""
echo "✅ System dependencies installed successfully!"
;;
fedora|rhel|centos)
echo "Detected: $PRETTY_NAME"
echo "Installing dependencies with dnf/yum..."
echo ""
# Try dnf first, fallback to yum
if command -v dnf &> /dev/null; then
PKG_MGR="dnf"
else
PKG_MGR="yum"
fi
$SUDO $PKG_MGR install -y \
alsa-lib \
atk \
at-spi2-atk \
cairo \
cups-libs \
dbus-libs \
expat \
fontconfig \
glib2 \
gtk3 \
libdrm \
libgbm \
libX11 \
libxcb \
libXcomposite \
libXcursor \
libXdamage \
libXext \
libXfixes \
libXi \
libxkbcommon \
libXrandr \
libXrender \
libXScrnSaver \
libXtst \
mesa-libgbm \
nspr \
nss \
pango
echo ""
echo "✅ System dependencies installed successfully!"
;;
arch|manjaro)
echo "Detected: $PRETTY_NAME"
echo "Installing dependencies with pacman..."
echo ""
$SUDO pacman -Sy --noconfirm \
alsa-lib \
at-spi2-core \
cairo \
cups \
dbus \
expat \
glib2 \
gtk3 \
libdrm \
libx11 \
libxcb \
libxcomposite \
libxcursor \
libxdamage \
libxext \
libxfixes \
libxi \
libxkbcommon \
libxrandr \
libxrender \
libxshmfence \
libxss \
libxtst \
mesa \
nspr \
nss \
pango
echo ""
echo "✅ System dependencies installed successfully!"
;;
*)
echo "❌ Unsupported OS: $OS"
echo " This script supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro"
echo ""
echo " Please install Chrome/Chromium dependencies manually for your OS."
echo " See: https://pptr.dev/troubleshooting"
exit 1
;;
esac
echo ""
echo "📝 Next steps:"
echo " 1. Run: cd $(dirname "$0")"
echo " 2. Run: npm install"
echo " 3. Test: node navigate.js --url https://example.com"
echo ""
#!/bin/bash
# Installation script for Chrome DevTools Agent Skill
set -e
echo "🚀 Installing Chrome DevTools Agent Skill..."
echo ""
# Check Node.js version
echo "Checking Node.js version..."
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "❌ Error: Node.js 18+ is required. Current version: $(node --version)"
echo " Please upgrade Node.js: https://nodejs.org/"
exit 1
fi
echo "✓ Node.js version: $(node --version)"
echo ""
# Check for system dependencies (Linux only)
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo "Checking system dependencies (Linux)..."
# Check for critical Chrome dependencies
MISSING_DEPS=()
if ! ldconfig -p | grep -q libnss3.so; then
MISSING_DEPS+=("libnss3")
fi
if ! ldconfig -p | grep -q libnspr4.so; then
MISSING_DEPS+=("libnspr4")
fi
if ! ldconfig -p | grep -q libgbm.so; then
MISSING_DEPS+=("libgbm1")
fi
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
echo "⚠️ Missing system dependencies: ${MISSING_DEPS[*]}"
echo ""
echo " Chrome/Chromium requires system libraries to run."
echo " Install them with:"
echo ""
echo " ./install-deps.sh"
echo ""
echo " Or manually:"
echo " sudo apt-get install -y libnss3 libnspr4 libgbm1 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2"
echo ""
read -p " Continue anyway? (y/N) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Installation cancelled."
exit 1
fi
else
echo "✓ System dependencies found"
fi
echo ""
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "Platform: macOS (no system dependencies needed)"
echo ""
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
echo "Platform: Windows (no system dependencies needed)"
echo ""
fi
# Install Node.js dependencies
echo "Installing Node.js dependencies..."
npm install
echo ""
echo "✅ Installation complete!"
echo ""
echo "Test the installation:"
echo " node navigate.js --url https://example.com"
echo ""
echo "For more information:"
echo " cat README.md"
echo ""
/**
* Shared browser utilities for Chrome DevTools scripts
*/
import puppeteer from 'puppeteer';
import debug from 'debug';
const log = debug('chrome-devtools:browser');
let browserInstance = null;
let pageInstance = null;
/**
* Launch or connect to browser
*/
export async function getBrowser(options = {}) {
if (browserInstance && browserInstance.isConnected()) {
log('Reusing existing browser instance');
return browserInstance;
}
const launchOptions = {
headless: options.headless !== false,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
...(options.args || [])
],
defaultViewport: options.viewport || {
width: 1920,
height: 1080
},
...options
};
if (options.browserUrl || options.wsEndpoint) {
log('Connecting to existing browser');
browserInstance = await puppeteer.connect({
browserURL: options.browserUrl,
browserWSEndpoint: options.wsEndpoint
});
} else {
log('Launching new browser');
browserInstance = await puppeteer.launch(launchOptions);
}
return browserInstance;
}
/**
* Get current page or create new one
*/
export async function getPage(browser) {
if (pageInstance && !pageInstance.isClosed()) {
log('Reusing existing page');
return pageInstance;
}
const pages = await browser.pages();
if (pages.length > 0) {
pageInstance = pages[0];
} else {
pageInstance = await browser.newPage();
}
return pageInstance;
}
/**
* Close browser
*/
export async function closeBrowser() {
if (browserInstance) {
await browserInstance.close();
browserInstance = null;
pageInstance = null;
}
}
/**
* Parse command line arguments
*/
export function parseArgs(argv, options = {}) {
const args = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith('--')) {
args[key] = nextArg;
i++;
} else {
args[key] = true;
}
}
}
return args;
}
/**
* Output JSON result
*/
export function outputJSON(data) {
console.log(JSON.stringify(data, null, 2));
}
/**
* Output error
*/
export function outputError(error) {
console.error(JSON.stringify({
success: false,
error: error.message,
stack: error.stack
}, null, 2));
process.exit(1);
}
#!/usr/bin/env node
/**
* Navigate to a URL
* Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function navigate() {
const args = parseArgs(process.argv.slice(2));
if (!args.url) {
outputError(new Error('--url is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
const options = {
waitUntil: args['wait-until'] || 'networkidle2',
timeout: parseInt(args.timeout || '30000')
};
await page.goto(args.url, options);
const result = {
success: true,
url: page.url(),
title: await page.title()
};
outputJSON(result);
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
navigate();
#!/usr/bin/env node
/**
* Monitor network requests
* Usage: node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
async function monitorNetwork() {
const args = parseArgs(process.argv.slice(2));
if (!args.url) {
outputError(new Error('--url is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
const requests = [];
const filterTypes = args.types ? args.types.split(',').map(t => t.toLowerCase()) : null;
// Monitor requests
page.on('request', (request) => {
const resourceType = request.resourceType().toLowerCase();
if (!filterTypes || filterTypes.includes(resourceType)) {
requests.push({
id: request._requestId || requests.length,
url: request.url(),
method: request.method(),
resourceType: resourceType,
headers: request.headers(),
postData: request.postData(),
timestamp: Date.now()
});
}
});
// Monitor responses
const responses = new Map();
page.on('response', async (response) => {
const request = response.request();
const resourceType = request.resourceType().toLowerCase();
if (!filterTypes || filterTypes.includes(resourceType)) {
try {
responses.set(request._requestId || request.url(), {
status: response.status(),
statusText: response.statusText(),
headers: response.headers(),
fromCache: response.fromCache(),
timing: response.timing()
});
} catch (e) {
// Ignore errors for some response types
}
}
});
// Navigate
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
// Merge requests with responses
const combined = requests.map(req => ({
...req,
response: responses.get(req.id) || responses.get(req.url) || null
}));
const result = {
success: true,
url: page.url(),
requestCount: combined.length,
requests: combined
};
if (args.output) {
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
outputJSON({
success: true,
output: args.output,
requestCount: combined.length
});
} else {
outputJSON(result);
}
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
monitorNetwork();
{
"name": "chrome-devtools-scripts",
"version": "1.0.0",
"description": "Browser automation scripts for Chrome DevTools Agent Skill",
"type": "module",
"scripts": {},
"dependencies": {
"puppeteer": "^24.15.0",
"debug": "^4.4.0",
"yargs": "^17.7.2"
},
"engines": {
"node": ">=18.0.0"
}
}
#!/usr/bin/env node
/**
* Measure performance metrics and record trace
* Usage: node performance.js --url https://example.com [--trace trace.json] [--metrics]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
async function measurePerformance() {
const args = parseArgs(process.argv.slice(2));
if (!args.url) {
outputError(new Error('--url is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Start tracing if requested
if (args.trace) {
await page.tracing.start({
path: args.trace,
categories: [
'devtools.timeline',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame'
]
});
}
// Navigate
await page.goto(args.url, {
waitUntil: 'networkidle2'
});
// Stop tracing
if (args.trace) {
await page.tracing.stop();
}
// Get performance metrics
const metrics = await page.metrics();
// Get Core Web Vitals
const vitals = await page.evaluate(() => {
return new Promise((resolve) => {
const vitals = {
LCP: null,
FID: null,
CLS: 0,
FCP: null,
TTFB: null
};
// LCP
try {
new PerformanceObserver((list) => {
const entries = list.getEntries();
if (entries.length > 0) {
const lastEntry = entries[entries.length - 1];
vitals.LCP = lastEntry.renderTime || lastEntry.loadTime;
}
}).observe({ entryTypes: ['largest-contentful-paint'], buffered: true });
} catch (e) {}
// CLS
try {
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
vitals.CLS += entry.value;
}
});
}).observe({ entryTypes: ['layout-shift'], buffered: true });
} catch (e) {}
// FCP
try {
const paintEntries = performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
if (fcpEntry) {
vitals.FCP = fcpEntry.startTime;
}
} catch (e) {}
// TTFB
try {
const [navigationEntry] = performance.getEntriesByType('navigation');
if (navigationEntry) {
vitals.TTFB = navigationEntry.responseStart - navigationEntry.requestStart;
}
} catch (e) {}
// Wait a bit for metrics to stabilize
setTimeout(() => resolve(vitals), 1000);
});
});
// Get resource timing
const resources = await page.evaluate(() => {
return performance.getEntriesByType('resource').map(r => ({
name: r.name,
type: r.initiatorType,
duration: r.duration,
size: r.transferSize,
startTime: r.startTime
}));
});
const result = {
success: true,
url: page.url(),
metrics: {
...metrics,
JSHeapUsedSizeMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2),
JSHeapTotalSizeMB: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2)
},
vitals: vitals,
resources: {
count: resources.length,
totalDuration: resources.reduce((sum, r) => sum + r.duration, 0),
items: args.resources === 'true' ? resources : undefined
}
};
if (args.trace) {
result.trace = args.trace;
}
outputJSON(result);
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
measurePerformance();
Chrome DevTools Scripts
CLI scripts for browser automation using Puppeteer.
Installation
Quick Install
cd .claude/skills/chrome-devtools/scripts
./install.sh # Auto-checks dependencies and installsManual Installation
Linux/WSL - Install system dependencies first:
./install-deps.sh # Auto-detects OS (Ubuntu, Debian, Fedora, etc.)Or manually:
sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1All platforms - Install Node dependencies:
npm installScripts
navigate.js
Navigate to a URL.
node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]screenshot.js
Take a screenshot.
Important: Always save screenshots to ./docs/screenshots directory.
node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element]click.js
Click an element.
node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]fill.js
Fill form fields.
node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true]evaluate.js
Execute JavaScript in page context.
node evaluate.js --script "document.title" [--url https://example.com]snapshot.js
Get DOM snapshot with interactive elements.
node snapshot.js [--url https://example.com] [--output snapshot.json]console.js
Monitor console messages.
node console.js --url https://example.com [--types error,warn] [--duration 5000]network.js
Monitor network requests.
node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]performance.js
Measure performance metrics and record trace.
node performance.js --url https://example.com [--trace trace.json] [--metrics] [--resources true]Common Options
--headless false- Show browser window--close false- Keep browser open--timeout 30000- Set timeout in milliseconds--wait-until networkidle2- Wait strategy (load, domcontentloaded, networkidle0, networkidle2)
Output Format
All scripts output JSON to stdout:
{
"success": true,
"url": "https://example.com",
"title": "Example Domain",
...
}Errors are output to stderr:
{
"success": false,
"error": "Error message",
"stack": "..."
}#!/usr/bin/env node
/**
* Take a screenshot
* Usage: node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
import path from 'path';
async function screenshot() {
const args = parseArgs(process.argv.slice(2));
if (!args.output) {
outputError(new Error('--output is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
const screenshotOptions = {
path: args.output,
type: args.format || 'png',
fullPage: args['full-page'] === 'true'
};
if (args.quality) {
screenshotOptions.quality = parseInt(args.quality);
}
let buffer;
if (args.selector) {
const element = await page.$(args.selector);
if (!element) {
throw new Error(`Element not found: ${args.selector}`);
}
buffer = await element.screenshot(screenshotOptions);
} else {
buffer = await page.screenshot(screenshotOptions);
}
const result = {
success: true,
output: path.resolve(args.output),
size: buffer.length,
url: page.url()
};
outputJSON(result);
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
screenshot();
#!/usr/bin/env node
/**
* Get DOM snapshot with selectors
* Usage: node snapshot.js [--url https://example.com] [--output snapshot.json]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
async function snapshot() {
const args = parseArgs(process.argv.slice(2));
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
// Get interactive elements with metadata
const elements = await page.evaluate(() => {
const interactiveSelectors = [
'a[href]',
'button',
'input',
'textarea',
'select',
'[onclick]',
'[role="button"]',
'[role="link"]',
'[contenteditable]'
];
const elements = [];
const selector = interactiveSelectors.join(', ');
const nodes = document.querySelectorAll(selector);
nodes.forEach((el, index) => {
const rect = el.getBoundingClientRect();
// Generate unique selector
let uniqueSelector = '';
if (el.id) {
uniqueSelector = `#${el.id}`;
} else if (el.className) {
const classes = Array.from(el.classList).join('.');
uniqueSelector = `${el.tagName.toLowerCase()}.${classes}`;
} else {
uniqueSelector = el.tagName.toLowerCase();
}
elements.push({
index: index,
tagName: el.tagName.toLowerCase(),
type: el.type || null,
id: el.id || null,
className: el.className || null,
name: el.name || null,
value: el.value || null,
text: el.textContent?.trim().substring(0, 100) || null,
href: el.href || null,
selector: uniqueSelector,
xpath: getXPath(el),
visible: rect.width > 0 && rect.height > 0,
position: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
}
});
});
function getXPath(element) {
if (element.id) {
return `//*[@id="${element.id}"]`;
}
if (element === document.body) {
return '/html/body';
}
let ix = 0;
const siblings = element.parentNode?.childNodes || [];
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i];
if (sibling === element) {
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
}
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
ix++;
}
}
return '';
}
return elements;
});
const result = {
success: true,
url: page.url(),
title: await page.title(),
elementCount: elements.length,
elements: elements
};
if (args.output) {
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
outputJSON({
success: true,
output: args.output,
elementCount: elements.length
});
} else {
outputJSON(result);
}
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
snapshot();