JavaScript Web Performance APIs
Performance optimization is a crucial aspect of modern web development. To effectively optimize your applications, you need tools to measure performance metrics. Fortunately, browsers provide several Web Performance APIs that help you measure, analyze, and optimize your web applications.
Introduction to Web Performance APIs
The Web Performance APIs are a collection of browser interfaces that enable you to access performance-related information about your web application. These APIs allow you to:
- Measure page load performance
- Track resource loading times
- Create custom performance markers and measures
- Monitor long tasks that might block the main thread
- Observe performance entries as they occur
Let's explore the key Web Performance APIs and learn how to use them effectively.
The Performance Interface
The performance
object is the entry point to most performance-related functionality in the browser. It's available in the global scope (window.performance
).
console.log(performance);
// Output: Performance {timeOrigin: 1234567890.123, onresourcetimingbufferfull: null, ...}
This object provides methods and properties that give you access to various performance measurements and metrics.
Navigation Timing API
The Navigation Timing API provides detailed timing information about the current page's load performance.
Getting Navigation Timing Data
// Get timing information about page navigation
const pageNavigation = performance.getEntriesByType('navigation')[0];
console.log(pageNavigation);
/* Output example:
PerformanceNavigationTiming {
name: "https://example.com/",
entryType: "navigation",
startTime: 0,
duration: 985.2000000476837,
initiatorType: "navigation",
nextHopProtocol: "h2",
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 2.3000000119209,
domainLookupStart: 2.3000000119209,
domainLookupEnd: 2.3000000119209,
connectStart: 2.3000000119209,
connectEnd: 2.3000000119209,
secureConnectionStart: 2.3000000119209,
requestStart: 13.300000011921,
responseStart: 132.60000002384,
responseEnd: 133.10000002384,
transferSize: 5527,
encodedBodySize: 5227,
decodedBodySize: 17450,
serverTiming: [],
workerTiming: [],
unloadEventStart: 0,
unloadEventEnd: 0,
domInteractive: 305.90000003576,
domContentLoadedEventStart: 305.90000003576,
domContentLoadedEventEnd: 306.00000003576,
domComplete: 985.0000000477,
loadEventStart: 985.1000000477,
loadEventEnd: 985.2000000477,
type: "navigate",
redirectCount: 0
}
*/
Common Navigation Timing Metrics
Here are some useful metrics you can calculate:
function calculatePageLoadMetrics() {
const navEntry = performance.getEntriesByType('navigation')[0];
// Time to First Byte (TTFB)
const ttfb = navEntry.responseStart - navEntry.startTime;
// Domain lookup time
const dnsTime = navEntry.domainLookupEnd - navEntry.domainLookupStart;
// Connection time
const connectionTime = navEntry.connectEnd - navEntry.connectStart;
// Document download time
const downloadTime = navEntry.responseEnd - navEntry.responseStart;
// DOM processing time
const domProcessingTime = navEntry.domComplete - navEntry.responseEnd;
// Total page load time
const pageLoadTime = navEntry.loadEventEnd - navEntry.startTime;
return {
ttfb,
dnsTime,
connectionTime,
downloadTime,
domProcessingTime,
pageLoadTime
};
}
console.log(calculatePageLoadMetrics());
/* Output example:
{
ttfb: 130.30000001192,
dnsTime: 0,
connectionTime: 0,
downloadTime: 0.5000000000000001,
domProcessingTime: 851.9000000238419,
pageLoadTime: 985.2000000476837
}
*/
Resource Timing API
The Resource Timing API provides detailed timing information for each resource loaded by your page, such as images, scripts, stylesheets, and XHR/fetch requests.
Getting Resource Timing Data
// Get timing information about all resources
const resources = performance.getEntriesByType('resource');
// Find a specific resource
const imageResource = resources.find(r => r.name.includes('logo.png'));
console.log(imageResource);
/* Output example:
PerformanceResourceTiming {
name: "https://example.com/images/logo.png",
entryType: "resource",
startTime: 145.30000001192,
duration: 85.59999999403954,
initiatorType: "img",
nextHopProtocol: "h2",
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 145.30000001192,
domainLookupStart: 145.30000001192,
domainLookupEnd: 145.30000001192,
connectStart: 145.30000001192,
connectEnd: 145.30000001192,
secureConnectionStart: 145.30000001192,
requestStart: 146.90000001192,
responseStart: 228.80000001192,
responseEnd: 230.90000000596,
transferSize: 4518,
encodedBodySize: 4218,
decodedBodySize: 23401,
serverTiming: []
}
*/
Analyzing Resource Loading Performance
This example shows how to analyze the performance of all resources loaded by category:
function analyzeResourcePerformance() {
const resources = performance.getEntriesByType('resource');
// Group resources by type
const categorized = {
script: [],
css: [],
image: [],
font: [],
other: []
};
resources.forEach(resource => {
const type = resource.initiatorType;
if (type === 'script') categorized.script.push(resource);
else if (type === 'css' || type === 'link') categorized.css.push(resource);
else if (type === 'img') categorized.image.push(resource);
else if (resource.name.match(/\.(woff|woff2|ttf|otf|eot)/)) categorized.font.push(resource);
else categorized.other.push(resource);
});
// Calculate total time and size for each category
const analysis = {};
for (const [category, items] of Object.entries(categorized)) {
const totalTime = items.reduce((sum, item) => sum + item.duration, 0);
const totalSize = items.reduce((sum, item) => sum + (item.transferSize || 0), 0);
analysis[category] = {
count: items.length,
totalTime: Math.round(totalTime),
totalSize: Math.round(totalSize / 1024), // KB
avgTime: items.length ? Math.round(totalTime / items.length) : 0
};
}
return analysis;
}
console.log(analyzeResourcePerformance());
/* Output example:
{
script: { count: 5, totalTime: 320, totalSize: 256, avgTime: 64 },
css: { count: 2, totalTime: 115, totalSize: 45, avgTime: 57 },
image: { count: 8, totalTime: 642, totalSize: 872, avgTime: 80 },
font: { count: 3, totalTime: 225, totalSize: 78, avgTime: 75 },
other: { count: 2, totalTime: 42, totalSize: 12, avgTime: 21 }
}
*/
User Timing API
The User Timing API allows you to create custom performance entries to measure specific operations in your code.
Creating Marks and Measures
// Create a mark to start measuring
performance.mark('startProcess');
// Simulate some work
for (let i = 0; i < 1000000; i++) {
// Heavy calculation
Math.sqrt(i);
}
// Create another mark to end measuring
performance.mark('endProcess');
// Create a measure between the two marks
performance.measure('processTime', 'startProcess', 'endProcess');
// Get the measure
const measure = performance.getEntriesByName('processTime')[0];
console.log(`Process took ${measure.duration.toFixed(2)} milliseconds`);
// Output example: Process took 15.43 milliseconds
// Clear marks and measures when done
performance.clearMarks();
performance.clearMeasures();
Practical Example: Measuring Function Execution Time
Here's a helper function to easily measure any function's execution time:
function measureFunctionPerformance(fn, fnName, ...args) {
const markStart = `${fnName}-start`;
const markEnd = `${fnName}-end`;
const measureName = `${fnName}-execution`;
performance.mark(markStart);
const result = fn(...args);
performance.mark(markEnd);
performance.measure(measureName, markStart, markEnd);
const measure = performance.getEntriesByName(measureName)[0];
console.log(`${fnName} took ${measure.duration.toFixed(2)} milliseconds`);
// Clean up
performance.clearMarks(markStart);
performance.clearMarks(markEnd);
performance.clearMeasures(measureName);
return result;
}
// Example usage
function findPrimes(max) {
const primes = [];
outer: for (let i = 2; i < max; i++) {
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) continue outer;
}
primes.push(i);
}
return primes;
}
const primes = measureFunctionPerformance(findPrimes, 'findPrimes', 10000);
// Output example: findPrimes took 12.54 milliseconds
Performance Observer
The PerformanceObserver interface allows you to observe performance entry types and get notified when new entries are created.
Basic Usage
// Create an observer that will watch for specific performance entry types
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
console.log(`${entry.name} (${entry.entryType}): ${entry.startTime.toFixed(2)}ms - duration: ${entry.duration.toFixed(2)}ms`);
}
});
// Start observing navigation, resource, and measure entries
observer.observe({ entryTypes: ['navigation', 'resource', 'measure'] });
// Later when you're done observing
// observer.disconnect();
Tracking Long Tasks
Long tasks (tasks that block the main thread for more than 50ms) can lead to poor user experience. You can use PerformanceObserver to detect these:
// Create an observer for long tasks
const longTaskObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
console.log(`Long task detected: ${entry.duration.toFixed(2)}ms at ${entry.startTime.toFixed(2)}ms`);
console.log('Attribution:', entry.attribution);
}
});
// Start observing long tasks
try {
longTaskObserver.observe({ entryTypes: ['longtask'] });
console.log('Long task observer started');
} catch (e) {
console.log('Long task observer not supported in this browser');
}
Paint Timing API
The Paint Timing API provides timing information about key rendering events.
// Get First Paint and First Contentful Paint metrics
const paintMetrics = performance.getEntriesByType('paint');
paintMetrics.forEach(metric => {
console.log(`${metric.name}: ${metric.startTime.toFixed(2)}ms`);
});
/* Output example:
first-paint: 124.55ms
first-contentful-paint: 124.55ms
*/
Practical Application: Performance Dashboard
Let's build a simple performance dashboard that collects and displays various metrics:
function createPerformanceDashboard() {
// Wait for load event
window.addEventListener('load', () => {
setTimeout(() => {
const dashboard = {};
// Navigation metrics
const navEntry = performance.getEntriesByType('navigation')[0];
dashboard.navigation = {
pageLoadTime: navEntry.loadEventEnd,
domContentLoaded: navEntry.domContentLoadedEventEnd,
timeToFirstByte: navEntry.responseStart,
domInteractive: navEntry.domInteractive
};
// Paint metrics
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach(entry => {
dashboard[entry.name] = entry.startTime;
});
// Resource metrics
const resources = performance.getEntriesByType('resource');
dashboard.resources = {
count: resources.length,
totalSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0) / 1024, // KB
totalTime: resources.reduce((max, r) => Math.max(max, r.responseEnd), 0)
};
// Display dashboard (in a real app, you'd display this in the UI)
console.table(dashboard);
// You could send this data to your analytics service
// sendToAnalytics(dashboard);
}, 0);
});
}
// Initialize dashboard
createPerformanceDashboard();
Summary
The JavaScript Web Performance APIs provide powerful tools for measuring and monitoring the performance of your web applications. Here's what we've covered:
- Navigation Timing API: Measure page load metrics like TTFB, DNS lookup, and total load time.
- Resource Timing API: Track individual resource loading performance.
- User Timing API: Create custom performance marks and measures.
- Performance Observer: Subscribe to performance events as they happen.
- Paint Timing API: Get information about when content is painted to the screen.
By leveraging these APIs, you can identify bottlenecks, measure the impact of optimizations, and ensure a smooth user experience.
Additional Resources
- MDN Web Performance APIs
- Web Vitals - Google's initiative for key metrics
- Chrome DevTools Performance Panel
Exercises
- Create a function that measures the time it takes to sort an array of 10,000 random numbers using different sorting algorithms.
- Build a visual dashboard that shows resource loading times by category (scripts, styles, images) in a bar chart.
- Use the PerformanceObserver to detect long tasks and display a warning in the console when tasks take more than 100ms.
- Implement a monitoring system that logs navigation metrics to localStorage every time a user visits your site.
Remember that performance optimization is an ongoing process. Regular monitoring and measurement will help you identify issues early and maintain a fast, responsive application.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)