JavaScript Profiling: Advanced Performance Guide
An advanced guide to JavaScript profiling and performance analysis. Covers the Performance API, PerformanceObserver, User Timing API, Long Tasks, Layout Shifts, resource timing, flame chart analysis, CPU profiling patterns, and building a custom performance monitor.
Profiling identifies where your JavaScript code spends time, which resources block rendering, and what operations cause layout shifts or long tasks. This guide covers the browser Performance APIs, custom timing instrumentation, and building automated monitoring.
For DevTools-specific workflows, see Using Chrome DevTools for JS Performance Tuning.
Performance API: High-Resolution Timing
// performance.now() provides sub-millisecond timestamps
function measureExecution(fn, label = "operation") {
const start = performance.now();
const result = fn();
const end = performance.now();
console.log(`${label}: ${(end - start).toFixed(3)}ms`);
return result;
}
// Async version
async function measureAsync(fn, label = "async-operation") {
const start = performance.now();
const result = await fn();
const end = performance.now();
console.log(`${label}: ${(end - start).toFixed(3)}ms`);
return result;
}
// Usage
measureExecution(() => {
const arr = Array.from({ length: 100000 }, (_, i) => i);
return arr.sort(() => Math.random() - 0.5);
}, "sort-100k");
await measureAsync(async () => {
return fetch("/api/data").then((r) => r.json());
}, "api-fetch");User Timing API (performance.mark and measure)
class PerformanceTracker {
constructor(prefix = "app") {
this.prefix = prefix;
}
mark(name) {
performance.mark(`${this.prefix}:${name}`);
}
measure(name, startMark, endMark) {
const measureName = `${this.prefix}:${name}`;
const start = `${this.prefix}:${startMark}`;
const end = `${this.prefix}:${endMark}`;
try {
performance.measure(measureName, start, end);
const entries = performance.getEntriesByName(measureName);
return entries[entries.length - 1];
} catch (error) {
console.warn("Measure failed:", error.message);
return null;
}
}
measureFromNavigation(name, markName) {
// Measure from page navigation start
const mark = `${this.prefix}:${markName}`;
const measureName = `${this.prefix}:${name}`;
performance.measure(measureName, { start: 0, end: mark });
return performance.getEntriesByName(measureName).pop();
}
getAllMeasures() {
return performance.getEntriesByType("measure")
.filter((e) => e.name.startsWith(this.prefix))
.map((e) => ({
name: e.name.replace(`${this.prefix}:`, ""),
duration: parseFloat(e.duration.toFixed(3)),
startTime: parseFloat(e.startTime.toFixed(3)),
}));
}
clear() {
performance.clearMarks();
performance.clearMeasures();
}
}
// Usage
const perf = new PerformanceTracker("myapp");
perf.mark("render-start");
renderComponent();
perf.mark("render-end");
const measure = perf.measure("render", "render-start", "render-end");
console.log(`Render took ${measure.duration.toFixed(2)}ms`);PerformanceObserver for Real-Time Monitoring
class PerformanceMonitor {
constructor() {
this.observers = [];
this.data = {
longTasks: [],
layoutShifts: [],
paints: [],
resources: [],
};
}
observeLongTasks() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.data.longTasks.push({
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
});
if (entry.duration > 100) {
console.warn(`Long task: ${entry.duration.toFixed(0)}ms`);
}
}
});
observer.observe({ type: "longtask", buffered: true });
this.observers.push(observer);
}
observeLayoutShifts() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
this.data.layoutShifts.push({
value: entry.value,
startTime: entry.startTime,
sources: entry.sources?.map((s) => ({
node: s.node?.tagName,
previousRect: s.previousRect,
currentRect: s.currentRect,
})),
});
}
}
});
observer.observe({ type: "layout-shift", buffered: true });
this.observers.push(observer);
}
observePaints() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.data.paints.push({
name: entry.name,
startTime: entry.startTime,
});
}
});
observer.observe({ type: "paint", buffered: true });
this.observers.push(observer);
}
observeResources(pattern = null) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (pattern && !entry.name.match(pattern)) continue;
this.data.resources.push({
name: entry.name,
type: entry.initiatorType,
duration: entry.duration,
transferSize: entry.transferSize,
decodedSize: entry.decodedBodySize,
});
}
});
observer.observe({ type: "resource", buffered: true });
this.observers.push(observer);
}
getCLS() {
return this.data.layoutShifts
.reduce((sum, shift) => sum + shift.value, 0);
}
getLongTaskCount() {
return this.data.longTasks.length;
}
getTotalBlockingTime() {
return this.data.longTasks.reduce((total, task) => {
return total + Math.max(0, task.duration - 50);
}, 0);
}
getReport() {
return {
cls: this.getCLS().toFixed(4),
longTasks: this.getLongTaskCount(),
totalBlockingTime: `${this.getTotalBlockingTime().toFixed(0)}ms`,
paints: this.data.paints,
slowResources: this.data.resources
.filter((r) => r.duration > 500)
.sort((a, b) => b.duration - a.duration)
.slice(0, 10),
};
}
disconnect() {
this.observers.forEach((o) => o.disconnect());
this.observers = [];
}
}
// Usage
const monitor = new PerformanceMonitor();
monitor.observeLongTasks();
monitor.observeLayoutShifts();
monitor.observePaints();
monitor.observeResources();
// Report after page is loaded
window.addEventListener("load", () => {
setTimeout(() => console.log(monitor.getReport()), 3000);
});Function-Level Profiling
function profileFunction(fn, name) {
let callCount = 0;
let totalTime = 0;
let maxTime = 0;
let minTime = Infinity;
const profiled = function (...args) {
const start = performance.now();
const result = fn.apply(this, args);
const elapsed = performance.now() - start;
callCount++;
totalTime += elapsed;
maxTime = Math.max(maxTime, elapsed);
minTime = Math.min(minTime, elapsed);
return result;
};
profiled.getStats = () => ({
name,
calls: callCount,
totalMs: totalTime.toFixed(3),
avgMs: callCount > 0 ? (totalTime / callCount).toFixed(3) : "0",
maxMs: maxTime.toFixed(3),
minMs: minTime === Infinity ? "0" : minTime.toFixed(3),
});
profiled.reset = () => {
callCount = 0;
totalTime = 0;
maxTime = 0;
minTime = Infinity;
};
return profiled;
}
// Profile async functions
function profileAsync(fn, name) {
let callCount = 0;
let totalTime = 0;
let maxTime = 0;
const profiled = async function (...args) {
const start = performance.now();
const result = await fn.apply(this, args);
const elapsed = performance.now() - start;
callCount++;
totalTime += elapsed;
maxTime = Math.max(maxTime, elapsed);
return result;
};
profiled.getStats = () => ({
name,
calls: callCount,
totalMs: totalTime.toFixed(3),
avgMs: callCount > 0 ? (totalTime / callCount).toFixed(3) : "0",
maxMs: maxTime.toFixed(3),
});
return profiled;
}
// Usage
const profiledSort = profileFunction(
(arr) => arr.slice().sort((a, b) => a - b),
"numericSort"
);
for (let i = 0; i < 100; i++) {
profiledSort(Array.from({ length: 10000 }, () => Math.random()));
}
console.table([profiledSort.getStats()]);Resource Timing Analysis
function analyzeResourceTiming() {
const resources = performance.getEntriesByType("resource");
const byType = {};
resources.forEach((r) => {
const type = r.initiatorType;
if (!byType[type]) {
byType[type] = { count: 0, totalDuration: 0, totalSize: 0 };
}
byType[type].count++;
byType[type].totalDuration += r.duration;
byType[type].totalSize += r.transferSize || 0;
});
// Format results
const report = Object.entries(byType).map(([type, stats]) => ({
type,
count: stats.count,
avgDurationMs: (stats.totalDuration / stats.count).toFixed(1),
totalSizeKB: (stats.totalSize / 1024).toFixed(1),
}));
return report.sort((a, b) => parseFloat(b.avgDurationMs) - parseFloat(a.avgDurationMs));
}
// Find slow resources
function findSlowResources(thresholdMs = 500) {
return performance.getEntriesByType("resource")
.filter((r) => r.duration > thresholdMs)
.map((r) => ({
url: new URL(r.name).pathname,
type: r.initiatorType,
duration: `${r.duration.toFixed(0)}ms`,
size: `${((r.transferSize || 0) / 1024).toFixed(1)}KB`,
ttfb: `${(r.responseStart - r.requestStart).toFixed(0)}ms`,
}))
.sort((a, b) => parseFloat(b.duration) - parseFloat(a.duration));
}
console.table(analyzeResourceTiming());
console.table(findSlowResources());| Timing Property | What It Measures |
|---|---|
startTime | When the resource request was initiated |
domainLookupEnd - domainLookupStart | DNS lookup time |
connectEnd - connectStart | TCP connection time |
responseStart - requestStart | Time to First Byte (TTFB) |
responseEnd - responseStart | Download time |
duration | Total time from start to response end |
transferSize | Bytes transferred over the network |
decodedBodySize | Decoded (uncompressed) body size |
Custom Performance Dashboard
class PerfDashboard {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.monitor = new PerformanceMonitor();
this.intervalId = null;
}
start() {
this.monitor.observeLongTasks();
this.monitor.observeLayoutShifts();
this.monitor.observePaints();
this.intervalId = setInterval(() => this.render(), 2000);
this.render();
}
render() {
const report = this.monitor.getReport();
const mem = performance.memory ? {
used: (performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1),
total: (performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1),
} : { used: "N/A", total: "N/A" };
this.container.innerHTML = `
<div class="perf-dashboard">
<div class="metric">
<span class="label">CLS</span>
<span class="value">${report.cls}</span>
</div>
<div class="metric">
<span class="label">Long Tasks</span>
<span class="value">${report.longTasks}</span>
</div>
<div class="metric">
<span class="label">TBT</span>
<span class="value">${report.totalBlockingTime}</span>
</div>
<div class="metric">
<span class="label">Heap Used</span>
<span class="value">${mem.used} MB</span>
</div>
<div class="metric">
<span class="label">Heap Total</span>
<span class="value">${mem.total} MB</span>
</div>
</div>
`;
}
stop() {
if (this.intervalId) clearInterval(this.intervalId);
this.monitor.disconnect();
}
}
// Usage
const dashboard = new PerfDashboard("perf-panel");
dashboard.start();Rune AI
Key Insights
- performance.now() for high-resolution timing: Use it instead of Date.now() for profiling; it provides microsecond precision and monotonic clock guarantees
- User Timing API for structured measurement: Create
mark()andmeasure()entries that appear in DevTools Performance recordings and can be collected by PerformanceObserver - PerformanceObserver for automated monitoring: Observe Long Tasks, Layout Shifts, and Resource Timing in real-time without polling, with buffered support for entries before observer registration
- Total Blocking Time quantifies responsiveness: Sum the time beyond 50ms for all Long Tasks to get TBT, a key metric that correlates with perceived input latency
- Function-level profiling wraps target functions: Create wrapper functions that track call count, total time, average, min, and max to identify hot functions without DevTools
Frequently Asked Questions
What is the difference between performance.now() and Date.now()?
What counts as a Long Task?
Can I profile Web Workers with the Performance API?
How do I correlate profiling data with user interactions?
Does the Performance API add overhead to my application?
Conclusion
JavaScript profiling combines the Performance API (performance.now(), mark(), measure()), PerformanceObserver (Long Tasks, Layout Shifts, Resource Timing), and custom instrumentation (function-level profiling, async measurement) to identify bottlenecks. Monitor CLS, TBT, and heap usage in real-time, automate measurements with PerformanceObserver, and correlate timing with user interactions. For DevTools-based profiling, see Using Chrome DevTools for JS Performance Tuning. For accurate timing techniques, see How to Measure JavaScript Execution Time Accurately.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.