Performance API: Precision Dashboard for Web Performance Optimization
Why We Need Performance API
Users don't like waiting. Research shows that for every second increase in page load time, user churn rate rises significantly. But before optimizing performance, you need to be able to measure it accurately—that's why Performance API exists.
Date.now() can tell you the time, but it's only millisecond-precise and susceptible to system clock adjustments. Performance API provides microsecond-level high-precision timing and rich performance metric collection capabilities, letting you clearly see the "skeletal structure" of your web application's performance, like an X-ray.
Performance Object Overview
The performance object is the entry point to Performance API, mounted on the window object:
// Get performance object
const perf = window.performance;
// View available APIs
console.log(
"Performance API:",
Object.getOwnPropertyNames(Performance.prototype)
);
// Basic properties
console.log("Time origin:", performance.timeOrigin);
// Absolute timestamp of page navigation start (Unix time)
console.log("Current time:", performance.now());
// High-precision time relative to timeOriginHigh-Precision Timing: performance.now()
performance.now() returns milliseconds since page navigation started, but with microsecond precision (subject to security restrictions):
// Basic usage
const startTime = performance.now();
// Execute some operations
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
const endTime = performance.now();
console.log(`Execution time: ${endTime - startTime}ms`);
// Output similar to: Execution time: 12.399999998509884ms
// Compare with Date.now()
const dateStart = Date.now();
const perfStart = performance.now();
// Date.now() only has millisecond precision
console.log("Date.now():", dateStart); // 1703841234567
console.log("performance.now():", perfStart); // 1234.567890Why It's More Precise
// Advantages of performance.now():
// 1. Not affected by system clock adjustments
// 2. Monotonically increasing, no time reversal
// 3. Microsecond precision (though browsers may reduce precision to prevent specific attacks)
function measureExecution(fn, iterations = 1000) {
const times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
const end = performance.now();
times.push(end - start);
}
// Calculate statistics
const sum = times.reduce((a, b) => a + b, 0);
const avg = sum / times.length;
const sorted = [...times].sort((a, b) => a - b);
const median = sorted[Math.floor(times.length / 2)];
const min = sorted[0];
const max = sorted[sorted.length - 1];
return { avg, median, min, max };
}
// Usage example
const stringConcatStats = measureExecution(() => {
let str = "";
for (let i = 0; i < 100; i++) {
str += "a";
}
});
console.log("String concatenation performance:", stringConcatStats);Performance Marks and Measures
performance.mark() - Create Marks
The mark() method can create marker points on the performance timeline:
// Create marks
performance.mark("app-start");
// Initialize application...
initializeApp();
performance.mark("app-initialized");
// Load data...
await loadData();
performance.mark("data-loaded");
// Render UI...
renderUI();
performance.mark("ui-rendered");
// View all marks
const marks = performance.getEntriesByType("mark");
console.table(
marks.map((m) => ({
name: m.name,
startTime: m.startTime.toFixed(2) + "ms",
}))
);performance.measure() - Measure Intervals
The measure() method can measure time between two marks:
// Create measure based on two marks
performance.mark("fetch-start");
await fetch("/api/data");
performance.mark("fetch-end");
performance.measure("API Call", "fetch-start", "fetch-end");
// Get measurement results
const measures = performance.getEntriesByType("measure");
measures.forEach((measure) => {
console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
});
// Can also measure from page start to a specific mark
performance.mark("first-paint-complete");
performance.measure("First Paint Time", undefined, "first-paint-complete");
// Or from a mark to current time
performance.mark("load-start");
// ...
performance.measure("Loading in progress", "load-start");Clearing Marks and Measures
// Clear specific mark
performance.clearMarks("app-start");
// Clear all marks
performance.clearMarks();
// Clear specific measure
performance.clearMeasures("API Call");
// Clear all measures
performance.clearMeasures();Practical Application: Performance Tracker
class PerformanceTracker {
constructor(prefix = "perf") {
this.prefix = prefix;
this.spans = new Map();
}
// Start a span
startSpan(name) {
const markName = `${this.prefix}:${name}:start`;
performance.mark(markName);
this.spans.set(name, markName);
return markName;
}
// End a span
endSpan(name) {
const startMark = this.spans.get(name);
if (!startMark) {
console.warn(`Span not found: ${name}`);
return null;
}
const endMark = `${this.prefix}:${name}:end`;
const measureName = `${this.prefix}:${name}`;
performance.mark(endMark);
performance.measure(measureName, startMark, endMark);
this.spans.delete(name);
const entries = performance.getEntriesByName(measureName, "measure");
return entries[entries.length - 1]?.duration;
}
// Measure function execution time
async measureAsync(name, fn) {
this.startSpan(name);
try {
return await fn();
} finally {
const duration = this.endSpan(name);
console.log(`[${name}] Duration: ${duration?.toFixed(2)}ms`);
}
}
// Sync version
measureSync(name, fn) {
this.startSpan(name);
try {
return fn();
} finally {
const duration = this.endSpan(name);
console.log(`[${name}] Duration: ${duration?.toFixed(2)}ms`);
}
}
// Get all measurement results
getAllMeasures() {
return performance
.getEntriesByType("measure")
.filter((m) => m.name.startsWith(this.prefix))
.map((m) => ({
name: m.name.replace(`${this.prefix}:`, ""),
duration: m.duration,
}));
}
// Get report
getReport() {
const measures = this.getAllMeasures();
const report = {};
measures.forEach((m) => {
if (!report[m.name]) {
report[m.name] = {
count: 0,
total: 0,
min: Infinity,
max: -Infinity,
};
}
report[m.name].count++;
report[m.name].total += m.duration;
report[m.name].min = Math.min(report[m.name].min, m.duration);
report[m.name].max = Math.max(report[m.name].max, m.duration);
});
// Calculate average
Object.values(report).forEach((r) => {
r.avg = r.total / r.count;
});
return report;
}
// Clean up
clear() {
performance.clearMarks();
performance.clearMeasures();
this.spans.clear();
}
}
// Usage example
const tracker = new PerformanceTracker("myApp");
// Track API call
await tracker.measureAsync("fetchUsers", async () => {
const response = await fetch("/api/users");
return response.json();
});
// Track rendering
tracker.measureSync("renderList", () => {
// Rendering logic
});
// Get report
console.table(tracker.getReport());Navigation Timing API
Navigation Timing provides detailed time information for each stage of page navigation:
// Get navigation time data
const navEntry = performance.getEntriesByType("navigation")[0];
if (navEntry) {
console.log("Navigation type:", navEntry.type);
// 'navigate', 'reload', 'back_forward', 'prerender'
// Key time points
console.log(
"DNS lookup time:",
navEntry.domainLookupEnd - navEntry.domainLookupStart
);
console.log("TCP connection time:", navEntry.connectEnd - navEntry.connectStart);
console.log("Request time:", navEntry.responseStart - navEntry.requestStart);
console.log("Response time:", navEntry.responseEnd - navEntry.responseStart);
console.log(
"DOM parsing time:",
navEntry.domContentLoadedEventEnd - navEntry.responseEnd
);
console.log("Total page load time:", navEntry.loadEventEnd - navEntry.startTime);
}
// Or use legacy API (better compatibility)
const timing = performance.timing;
if (timing) {
console.log("Page load time:", timing.loadEventEnd - timing.navigationStart);
console.log(
"DOM ready time:",
timing.domContentLoadedEventEnd - timing.navigationStart
);
}Page Load Metrics Visualization
function visualizePageLoadTiming() {
const nav = performance.getEntriesByType("navigation")[0];
if (!nav) {
console.log("Navigation timing data not available");
return;
}
const timeline = [
{
phase: "DNS Lookup",
start: nav.domainLookupStart,
end: nav.domainLookupEnd,
},
{ phase: "TCP Connection", start: nav.connectStart, end: nav.connectEnd },
{
phase: "SSL Handshake",
start: nav.secureConnectionStart || nav.connectStart,
end: nav.connectEnd,
},
{ phase: "Request Sent", start: nav.requestStart, end: nav.responseStart },
{ phase: "Response Received", start: nav.responseStart, end: nav.responseEnd },
{ phase: "DOM Parsing", start: nav.responseEnd, end: nav.domInteractive },
{
phase: "DOMContentLoaded",
start: nav.domContentLoadedEventStart,
end: nav.domContentLoadedEventEnd,
},
{ phase: "Load Event", start: nav.loadEventStart, end: nav.loadEventEnd },
];
console.log("\n📊 Page Load Timeline:");
console.log("=".repeat(60));
timeline.forEach(({ phase, start, end }) => {
const duration = end - start;
if (duration > 0) {
const bar = "█".repeat(Math.min(Math.round(duration / 10), 40));
console.log(`${phase.padEnd(18)} ${bar} ${duration.toFixed(2)}ms`);
}
});
console.log("=".repeat(60));
console.log(`Total load time: ${nav.loadEventEnd.toFixed(2)}ms`);
}
// Execute after page load
window.addEventListener("load", () => {
setTimeout(visualizePageLoadTiming, 0);
});Resource Timing API
Resource Timing provides detailed load time information for each resource:
// Get all resource load times
const resources = performance.getEntriesByType("resource");
console.log(`Loaded ${resources.length} resources`);
resources.forEach((resource) => {
console.log({
name: resource.name,
type: resource.initiatorType,
duration: resource.duration.toFixed(2) + "ms",
size: resource.transferSize + " bytes",
});
});
// Group statistics by type
function analyzeResourcesByType() {
const resources = performance.getEntriesByType("resource");
const byType = {};
resources.forEach((r) => {
if (!byType[r.initiatorType]) {
byType[r.initiatorType] = {
count: 0,
totalDuration: 0,
totalSize: 0,
};
}
byType[r.initiatorType].count++;
byType[r.initiatorType].totalDuration += r.duration;
byType[r.initiatorType].totalSize += r.transferSize || 0;
});
return byType;
}
console.table(analyzeResourcesByType());Find Slow Resources
function findSlowResources(threshold = 500) {
const resources = performance.getEntriesByType("resource");
const slowResources = resources
.filter((r) => r.duration > threshold)
.sort((a, b) => b.duration - a.duration)
.map((r) => ({
url: r.name.split("/").pop(), // Show filename only
fullUrl: r.name,
type: r.initiatorType,
duration: r.duration.toFixed(2) + "ms",
size: (r.transferSize / 1024).toFixed(2) + "KB",
}));
if (slowResources.length > 0) {
console.warn(
`⚠️ Found ${slowResources.length} slow resources (>${threshold}ms):`
);
console.table(slowResources);
} else {
console.log(`✓ All resources loaded within ${threshold}ms`);
}
return slowResources;
}
// Check after page load
window.addEventListener("load", () => {
setTimeout(() => findSlowResources(300), 100);
});First Paint and Largest Contentful Paint
Modern browsers provide Paint Timing API to measure key rendering metrics:
// Get paint timings
function getPaintTimings() {
const paintEntries = performance.getEntriesByType("paint");
const timings = {};
paintEntries.forEach((entry) => {
timings[entry.name] = entry.startTime;
});
console.log("First Paint:", timings["first-paint"]?.toFixed(2) + "ms");
console.log(
"First Contentful Paint:",
timings["first-contentful-paint"]?.toFixed(2) + "ms"
);
return timings;
}
// Use PerformanceObserver to listen for LCP
function observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(
"Largest Contentful Paint:",
lastEntry.startTime.toFixed(2) + "ms"
);
console.log("LCP element:", lastEntry.element);
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
}
// Check after page load
window.addEventListener("load", () => {
getPaintTimings();
observeLCP();
});PerformanceObserver
PerformanceObserver can asynchronously observe creation of performance entries:
// Observe all performance entries
const observer = new PerformanceObserver((list, observer) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(
`[${entry.entryType}] ${entry.name}: ${
entry.duration?.toFixed(2) || entry.startTime.toFixed(2)
}ms`
);
});
});
// Observe multiple types
observer.observe({
entryTypes: ["mark", "measure", "resource", "navigation"],
});
// Observe specific type only (more efficient)
const resourceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 1000) {
console.warn(`Slow resource: ${entry.name} (${entry.duration.toFixed(0)}ms)`);
}
});
});
resourceObserver.observe({ type: "resource", buffered: true });
// Stop observing
// observer.disconnect();Monitor Long Tasks
// Monitor long tasks (tasks exceeding 50ms)
function observeLongTasks() {
if (!PerformanceObserver.supportedEntryTypes.includes("longtask")) {
console.log("Browser doesn't support Long Task monitoring");
return;
}
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.warn("⚠️ Long task detected:", {
duration: entry.duration.toFixed(2) + "ms",
startTime: entry.startTime.toFixed(2) + "ms",
name: entry.name,
});
});
});
observer.observe({ type: "longtask", buffered: true });
return observer;
}
observeLongTasks();Performance Monitoring System
Combine the above APIs to build a complete performance monitoring system:
class PerformanceMonitor {
constructor(options = {}) {
this.metrics = {};
this.options = {
reportUrl: options.reportUrl,
sampleRate: options.sampleRate || 1,
...options,
};
this.init();
}
init() {
// Sampling check
if (Math.random() > this.options.sampleRate) {
return;
}
this.collectNavigationTiming();
this.collectPaintTiming();
this.observeResources();
this.observeLongTasks();
this.collectWebVitals();
// Send data on page unload
window.addEventListener("beforeunload", () => {
this.sendReport();
});
// Or send periodically
// setInterval(() => this.sendReport(), 60000);
}
collectNavigationTiming() {
window.addEventListener("load", () => {
setTimeout(() => {
const nav = performance.getEntriesByType("navigation")[0];
if (nav) {
this.metrics.navigation = {
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
ttfb: nav.responseStart - nav.requestStart,
download: nav.responseEnd - nav.responseStart,
domParse: nav.domInteractive - nav.responseEnd,
domReady: nav.domContentLoadedEventEnd - nav.startTime,
load: nav.loadEventEnd - nav.startTime,
};
}
}, 0);
});
}
collectPaintTiming() {
window.addEventListener("load", () => {
setTimeout(() => {
const paints = performance.getEntriesByType("paint");
paints.forEach((paint) => {
this.metrics[paint.name] = paint.startTime;
});
}, 0);
});
}
observeResources() {
const observer = new PerformanceObserver((list) => {
if (!this.metrics.resources) {
this.metrics.resources = {
count: 0,
totalSize: 0,
totalDuration: 0,
slow: [],
};
}
list.getEntries().forEach((entry) => {
this.metrics.resources.count++;
this.metrics.resources.totalSize += entry.transferSize || 0;
this.metrics.resources.totalDuration += entry.duration;
if (entry.duration > 1000) {
this.metrics.resources.slow.push({
url: entry.name,
duration: entry.duration,
size: entry.transferSize,
});
}
});
});
observer.observe({ type: "resource", buffered: true });
}
observeLongTasks() {
if (!PerformanceObserver.supportedEntryTypes.includes("longtask")) {
return;
}
this.metrics.longTasks = [];
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.metrics.longTasks.push({
startTime: entry.startTime,
duration: entry.duration,
});
});
});
observer.observe({ type: "longtask", buffered: true });
}
collectWebVitals() {
// LCP
if (
PerformanceObserver.supportedEntryTypes.includes(
"largest-contentful-paint"
)
) {
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
this.metrics.lcp = entries[entries.length - 1]?.startTime;
});
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
}
// FID (requires user interaction to trigger)
if (PerformanceObserver.supportedEntryTypes.includes("first-input")) {
const fidObserver = new PerformanceObserver((list) => {
const entry = list.getEntries()[0];
this.metrics.fid = entry.processingStart - entry.startTime;
});
fidObserver.observe({ type: "first-input", buffered: true });
}
// CLS
if (PerformanceObserver.supportedEntryTypes.includes("layout-shift")) {
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
this.metrics.cls = clsValue;
});
clsObserver.observe({ type: "layout-shift", buffered: true });
}
}
getReport() {
return {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
metrics: this.metrics,
};
}
sendReport() {
const report = this.getReport();
if (this.options.reportUrl) {
// Use sendBeacon to ensure data is sent
const blob = new Blob([JSON.stringify(report)], {
type: "application/json",
});
navigator.sendBeacon(this.options.reportUrl, blob);
}
console.log("📊 Performance report:", report);
return report;
}
}
// Usage example
const monitor = new PerformanceMonitor({
reportUrl: "/api/performance",
sampleRate: 0.1, // 10% sampling rate
});
// Manually get report
setTimeout(() => {
console.table(monitor.getReport().metrics);
}, 5000);Performance Optimization Practices
Identify Performance Bottlenecks
function diagnosePerformance() {
const nav = performance.getEntriesByType("navigation")[0];
const issues = [];
// Check DNS
if (nav.domainLookupEnd - nav.domainLookupStart > 100) {
issues.push({
type: "DNS",
message: "DNS lookup time too long, consider using DNS prefetch",
solution: '<link rel="dns-prefetch" href="//your-domain.com">',
});
}
// Check TTFB
if (nav.responseStart - nav.requestStart > 500) {
issues.push({
type: "TTFB",
message: "TTFB too long, slow server response",
solution: "Optimize server-side processing, use CDN, enable caching",
});
}
// Check resource size
const resources = performance.getEntriesByType("resource");
const largeResources = resources.filter(
(r) => (r.transferSize || 0) > 500 * 1024
);
if (largeResources.length > 0) {
issues.push({
type: "RESOURCE_SIZE",
message: `${largeResources.length} resources over 500KB`,
solution: "Compress resources, optimize images, lazy load",
details: largeResources.map((r) => r.name),
});
}
// Check resource count
if (resources.length > 100) {
issues.push({
type: "RESOURCE_COUNT",
message: `Too many resource requests (${resources.length})`,
solution: "Merge resources, use HTTP/2, lazy loading",
});
}
return issues;
}
// Run diagnostics
window.addEventListener("load", () => {
setTimeout(() => {
const issues = diagnosePerformance();
if (issues.length > 0) {
console.warn("🔍 Performance issues found:");
issues.forEach((issue) => {
console.group(`❌ ${issue.type}`);
console.log("Problem:", issue.message);
console.log("Suggestion:", issue.solution);
if (issue.details) {
console.log("Details:", issue.details);
}
console.groupEnd();
});
} else {
console.log("✅ Performance check passed!");
}
}, 1000);
});Summary
Performance API is an important tool for building high-performance web applications. It provides comprehensive capabilities from coarse-grained page load times to fine-grained resource load analysis, from simple code execution timing to complex Web Vitals metric collection.
Key takeaways:
performance.now()provides high-precision timingmark()andmeasure()for custom performance markers- Navigation Timing analyzes page load stages
- Resource Timing tracks each resource's load performance
- PerformanceObserver implements asynchronous performance monitoring
- Build complete performance monitoring systems with Web Vitals
Master these tools to accurately identify performance bottlenecks, make data-driven optimization decisions, and ultimately provide users with a smooth and fast experience.