Initial commit
This commit is contained in:
940
skills/chrome-devtools/references/performance-guide.md
Normal file
940
skills/chrome-devtools/references/performance-guide.md
Normal file
@@ -0,0 +1,940 @@
|
||||
# Performance Analysis Guide
|
||||
|
||||
Comprehensive guide to analyzing web performance using Chrome DevTools Protocol, Puppeteer, and chrome-devtools skill.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Core Web Vitals](#core-web-vitals)
|
||||
- [Performance Tracing](#performance-tracing)
|
||||
- [Network Analysis](#network-analysis)
|
||||
- [JavaScript Performance](#javascript-performance)
|
||||
- [Rendering Performance](#rendering-performance)
|
||||
- [Memory Analysis](#memory-analysis)
|
||||
- [Optimization Strategies](#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
|
||||
|
||||
```javascript
|
||||
// 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 recommendations
|
||||
```
|
||||
|
||||
### Measuring with Puppeteer
|
||||
|
||||
```javascript
|
||||
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)**
|
||||
```javascript
|
||||
const ttfb = await page.evaluate(() => {
|
||||
const [navigationEntry] = performance.getEntriesByType('navigation');
|
||||
return navigationEntry.responseStart - navigationEntry.requestStart;
|
||||
});
|
||||
```
|
||||
|
||||
**FCP (First Contentful Paint)**
|
||||
```javascript
|
||||
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)**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
// 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 visualization
|
||||
```
|
||||
|
||||
### JavaScript Coverage
|
||||
|
||||
**Identify Unused Code:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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:**
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
- [Web.dev Performance](https://web.dev/performance/)
|
||||
- [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/)
|
||||
- [Core Web Vitals](https://web.dev/vitals/)
|
||||
- [Lighthouse](https://developer.chrome.com/docs/lighthouse/)
|
||||
- [WebPageTest](https://www.webpagetest.org/)
|
||||
Reference in New Issue
Block a user