Performance Profiling: DevTools and Metrics in JavaScript
Performance profiling is essential for building fast, responsive applications. This article covers using Chrome DevTools to measure performance, understand metrics, identify bottlenecks, and optimize your code.
Introduction
Modern web applications need to be fast. Users expect:
- Pages to load in under 3 seconds
- Interactions to respond within 100ms
- Smooth 60fps animations
- Minimal battery drain on mobile
Performance profiling helps you:
- Identify slow code sections
- Measure real-world performance
- Track performance over time
- Optimize before users notice issues
Chrome DevTools Performance Tab
Opening DevTools
// Open DevTools
// Windows/Linux: F12 or Ctrl+Shift+I
// Mac: Cmd+Option+I
// Or right-click โ Inspect โ Performance tab
Recording a Performance Profile
// Example: Slow function to profile
function slowFunction() {
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += Math.sqrt(i);
}
return sum;
}
// Steps to profile:
// 1. Open DevTools โ Performance tab
// 2. Click Record (or Ctrl+Shift+E)
// 3. Perform actions in your app
// 4. Click Stop
// 5. Analyze the results
Understanding the Timeline
// The performance timeline shows:
// - Network requests (blue)
// - Scripting (yellow)
// - Rendering (purple)
// - Painting (green)
// - Idle time (gray)
// Example: Monitoring a data fetch
async function fetchAndRender() {
const response = await fetch('/api/data');
const data = await response.json();
// This rendering work will show in the timeline
renderData(data);
}
Performance Metrics
Core Web Vitals
// Largest Contentful Paint (LCP)
// Measures when the largest content element is visible
// Target: < 2.5 seconds
// First Input Delay (FID)
// Measures responsiveness to user input
// Target: < 100 milliseconds
// Cumulative Layout Shift (CLS)
// Measures visual stability
// Target: < 0.1
// Measuring LCP
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP:', entry.renderTime || entry.loadTime);
}
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
Navigation Timing API
// Get detailed timing information
const perfData = performance.getEntriesByType('navigation')[0];
console.log('DNS lookup:', perfData.domainLookupEnd - perfData.domainLookupStart);
console.log('TCP connection:', perfData.connectEnd - perfData.connectStart);
console.log('Request time:', perfData.responseStart - perfData.requestStart);
console.log('Response time:', perfData.responseEnd - perfData.responseStart);
console.log('DOM parsing:', perfData.domInteractive - perfData.domLoading);
console.log('Total load time:', perfData.loadEventEnd - perfData.fetchStart);
// Practical example: Measuring page load performance
function analyzePageLoad() {
const perfData = performance.getEntriesByType('navigation')[0];
const metrics = {
dns: perfData.domainLookupEnd - perfData.domainLookupStart,
tcp: perfData.connectEnd - perfData.connectStart,
ttfb: perfData.responseStart - perfData.fetchStart,
download: perfData.responseEnd - perfData.responseStart,
domParse: perfData.domInteractive - perfData.domLoading,
resources: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
total: perfData.loadEventEnd - perfData.fetchStart
};
console.log('Page Load Metrics:', metrics);
return metrics;
}
User Timing API
// Mark custom performance points
performance.mark('operation-start');
// Do some work
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
performance.mark('operation-end');
// Measure the time between marks
performance.measure('operation', 'operation-start', 'operation-end');
// Get the measurement
const measure = performance.getEntriesByName('operation')[0];
console.log('Operation took:', measure.duration, 'ms');
// Practical example: Measuring function performance
function measureFunctionPerformance(fn, label) {
const startMark = `${label}-start`;
const endMark = `${label}-end`;
const measureName = `${label}-duration`;
performance.mark(startMark);
fn();
performance.mark(endMark);
performance.measure(measureName, startMark, endMark);
const measure = performance.getEntriesByName(measureName)[0];
console.log(`${label} took ${measure.duration.toFixed(2)}ms`);
return measure.duration;
}
// Usage
measureFunctionPerformance(() => {
// Your code here
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
}, 'calculation');
Profiling Techniques
CPU Profiling
// Steps to profile CPU usage:
// 1. Open DevTools โ Performance tab
// 2. Click Record
// 3. Perform the action
// 4. Click Stop
// 5. Look at the flame graph
// The flame graph shows:
// - Width = time spent
// - Height = call stack depth
// - Color = function type
// Example: Function that shows up in CPU profile
function expensiveCalculation(n) {
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i) * Math.sin(i);
}
return result;
}
// Call it multiple times
for (let i = 0; i < 100; i++) {
expensiveCalculation(1000000);
}
Memory Profiling
// Heap snapshot: Captures memory at a point in time
// Steps:
// 1. Open DevTools โ Memory tab
// 2. Click "Take heap snapshot"
// 3. Perform actions
// 4. Take another snapshot
// 5. Compare snapshots
// Example: Memory leak
let leakedArray = [];
function createLeak() {
const largeArray = new Array(1000000).fill('data');
leakedArray.push(largeArray);
}
// This creates a memory leak
for (let i = 0; i < 100; i++) {
createLeak();
}
// Allocation timeline: Shows memory allocation over time
// Steps:
// 1. Open DevTools โ Memory tab
// 2. Select "Allocation timeline"
// 3. Click Start
// 4. Perform actions
// 5. Click Stop
// 6. Analyze the timeline
// Example: Monitoring memory during operations
class DataManager {
constructor() {
this.data = [];
}
addData(items) {
this.data.push(...items);
}
clearData() {
this.data = [];
}
}
const manager = new DataManager();
// Monitor memory as we add data
for (let i = 0; i < 1000; i++) {
manager.addData(new Array(10000).fill(i));
}
Performance Observer API
Observing Performance Entries
// Create a performance observer
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Performance entry:', entry);
}
});
// Observe different entry types
observer.observe({
entryTypes: ['measure', 'navigation', 'resource', 'paint']
});
// Practical example: Monitoring all performance metrics
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.setupObserver();
}
setupObserver() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.recordMetric(entry);
}
});
observer.observe({
entryTypes: ['measure', 'navigation', 'resource', 'paint', 'largest-contentful-paint']
});
}
recordMetric(entry) {
if (!this.metrics[entry.entryType]) {
this.metrics[entry.entryType] = [];
}
this.metrics[entry.entryType].push({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
}
getReport() {
return this.metrics;
}
}
// Usage
const monitor = new PerformanceMonitor();
Analyzing Results
Identifying Bottlenecks
// Look for:
// 1. Long tasks (> 50ms)
// 2. Excessive garbage collection
// 3. Layout thrashing
// 4. Forced reflows
// Example: Layout thrashing (bad)
function badLayoutThrashing() {
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
el.style.width = el.offsetWidth + 10 + 'px'; // Read
// Triggers reflow
el.style.height = el.offsetHeight + 10 + 'px'; // Read
// Triggers reflow
});
}
// Example: Optimized (good)
function goodLayoutOptimization() {
const elements = document.querySelectorAll('.item');
const measurements = [];
// Read all values first
elements.forEach(el => {
measurements.push({
width: el.offsetWidth,
height: el.offsetHeight
});
});
// Then write all values
elements.forEach((el, i) => {
el.style.width = measurements[i].width + 10 + 'px';
el.style.height = measurements[i].height + 10 + 'px';
});
}
Comparing Profiles
// Steps to compare two profiles:
// 1. Record profile A
// 2. Make optimization
// 3. Record profile B
// 4. Compare metrics
// Example: Before and after optimization
function beforeOptimization() {
performance.mark('before-start');
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += Math.sqrt(i);
}
performance.mark('before-end');
performance.measure('before', 'before-start', 'before-end');
}
function afterOptimization() {
performance.mark('after-start');
// Optimized version
let sum = 0;
const sqrtCache = {};
for (let i = 0; i < 100000000; i++) {
if (!sqrtCache[i]) {
sqrtCache[i] = Math.sqrt(i);
}
sum += sqrtCache[i];
}
performance.mark('after-end');
performance.measure('after', 'after-start', 'after-end');
}
Practical Examples
Profiling a React Component
// Measure component render time
function ProfiledComponent() {
const startTime = performance.now();
// Component logic
const [data, setData] = React.useState([]);
React.useEffect(() => {
performance.mark('data-fetch-start');
fetch('/api/data')
.then(r => r.json())
.then(data => {
setData(data);
performance.mark('data-fetch-end');
performance.measure('data-fetch', 'data-fetch-start', 'data-fetch-end');
});
}, []);
const renderTime = performance.now() - startTime;
console.log('Component render time:', renderTime);
return <div>{/* Component JSX */}</div>;
}
Monitoring API Response Times
// Wrap fetch to measure response times
async function fetchWithMetrics(url) {
const startTime = performance.now();
try {
const response = await fetch(url);
const data = await response.json();
const duration = performance.now() - startTime;
console.log(`Fetch ${url} took ${duration.toFixed(2)}ms`);
// Record metric
performance.mark(`fetch-${url}`);
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
// Usage
const data = await fetchWithMetrics('/api/users');
Analyzing Long Tasks
// Detect long tasks (> 50ms)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long task detected:', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
Best Practices
-
Profile regularly:
// โ Good - measure performance continuously const monitor = new PerformanceMonitor(); -
Use meaningful marks:
// โ Good - descriptive names performance.mark('user-interaction-start'); -
Compare before and after:
// โ Good - measure optimization impact const before = performance.now(); optimizedFunction(); const after = performance.now(); -
Monitor in production:
// โ Good - track real-world performance sendMetricsToAnalytics(performanceData);
Common Mistakes
-
Not profiling real-world scenarios:
// โ Bad - only testing in ideal conditions // โ Good - test with real data and network conditions -
Ignoring memory leaks:
// โ Bad - not monitoring memory // โ Good - regularly check heap snapshots -
Optimizing the wrong thing:
// โ Bad - optimize without profiling // โ Good - profile first, then optimize
Summary
Performance profiling is essential for building fast applications. Key takeaways:
- Use Chrome DevTools Performance tab to record and analyze
- Understand Core Web Vitals (LCP, FID, CLS)
- Use User Timing API for custom measurements
- Profile CPU and memory usage
- Identify and fix bottlenecks
- Monitor performance in production
- Compare before and after optimizations
Related Resources
- Chrome DevTools Performance - Google
- Performance API - MDN
- Web Vitals - Google
- User Timing API - MDN
- Performance Observer API - MDN
Next Steps
- Learn about Memory Optimization and Leak Detection
- Explore Rendering Performance: Reflow and Repaint
- Study Web Vitals and Performance Metrics
- Practice profiling your own applications
- Monitor performance in production environments
Comments