javascript
examples
examples.js⚡javascript
/**
* 19.5 Performance Observer - Examples
*
* Monitor and analyze performance metrics
*/
// ============================================
// BASIC PERFORMANCE OBSERVER
// ============================================
/**
* Simple performance monitoring
*/
function basicPerformanceObserver() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('=== Performance Entry ===');
console.log('Name:', entry.name);
console.log('Type:', entry.entryType);
console.log('Duration:', entry.duration.toFixed(2), 'ms');
console.log('Start Time:', entry.startTime.toFixed(2), 'ms');
}
});
// Observe multiple entry types
try {
observer.observe({ entryTypes: ['mark', 'measure', 'resource'] });
} catch (e) {
console.warn('Some entry types not supported');
}
return observer;
}
// ============================================
// CUSTOM MARKS AND MEASURES
// ============================================
/**
* Performance timing utility
*/
class PerformanceTimer {
constructor() {
this.marks = new Map();
this.measures = [];
}
start(name) {
const markName = `${name}-start`;
performance.mark(markName);
this.marks.set(name, markName);
return this;
}
end(name, options = {}) {
const startMark = this.marks.get(name);
if (!startMark) {
console.warn(`No start mark found for: ${name}`);
return null;
}
const endMark = `${name}-end`;
performance.mark(endMark);
const measure = performance.measure(name, startMark, endMark);
this.measures.push({
name,
duration: measure.duration,
startTime: measure.startTime,
});
if (!options.keepMarks) {
performance.clearMarks(startMark);
performance.clearMarks(endMark);
}
return measure.duration;
}
measure(name, callback) {
this.start(name);
const result = callback();
if (result instanceof Promise) {
return result.finally(() => this.end(name));
}
this.end(name);
return result;
}
async measureAsync(name, asyncCallback) {
this.start(name);
try {
return await asyncCallback();
} finally {
this.end(name);
}
}
getReport() {
return this.measures.map((m) => ({
...m,
durationFormatted: `${m.duration.toFixed(2)}ms`,
}));
}
clear() {
this.marks.clear();
this.measures = [];
performance.clearMarks();
performance.clearMeasures();
}
}
// ============================================
// CORE WEB VITALS MONITORING
// ============================================
/**
* Monitor Core Web Vitals
*/
function monitorWebVitals(callback) {
const vitals = {
LCP: null,
FID: null,
CLS: null,
FCP: null,
TTFB: null,
};
// Largest Contentful Paint
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
vitals.LCP = lastEntry.startTime;
callback('LCP', vitals.LCP, getRating('LCP', vitals.LCP));
});
try {
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
} catch (e) {
console.warn('LCP not supported');
}
// First Input Delay
const fidObserver = new PerformanceObserver((list) => {
const entry = list.getEntries()[0];
vitals.FID = entry.processingStart - entry.startTime;
callback('FID', vitals.FID, getRating('FID', vitals.FID));
});
try {
fidObserver.observe({ type: 'first-input', buffered: true });
} catch (e) {
console.warn('FID not supported');
}
// Cumulative Layout Shift
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
vitals.CLS = clsValue;
callback('CLS', vitals.CLS, getRating('CLS', vitals.CLS));
});
try {
clsObserver.observe({ type: 'layout-shift', buffered: true });
} catch (e) {
console.warn('CLS not supported');
}
// First Contentful Paint
const fcpObserver = new PerformanceObserver((list) => {
const entry = list
.getEntries()
.find((e) => e.name === 'first-contentful-paint');
if (entry) {
vitals.FCP = entry.startTime;
callback('FCP', vitals.FCP, getRating('FCP', vitals.FCP));
}
});
try {
fcpObserver.observe({ type: 'paint', buffered: true });
} catch (e) {
console.warn('Paint timing not supported');
}
// Time to First Byte
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry) {
vitals.TTFB = navEntry.responseStart;
callback('TTFB', vitals.TTFB, getRating('TTFB', vitals.TTFB));
}
function getRating(metric, value) {
const thresholds = {
LCP: [2500, 4000],
FID: [100, 300],
CLS: [0.1, 0.25],
FCP: [1800, 3000],
TTFB: [800, 1800],
};
const [good, poor] = thresholds[metric] || [0, 0];
if (value <= good) return 'good';
if (value <= poor) return 'needs-improvement';
return 'poor';
}
return {
getVitals: () => ({ ...vitals }),
disconnect: () => {
lcpObserver.disconnect();
fidObserver.disconnect();
clsObserver.disconnect();
fcpObserver.disconnect();
},
};
}
// ============================================
// RESOURCE TIMING ANALYSIS
// ============================================
/**
* Analyze resource loading performance
*/
class ResourceAnalyzer {
constructor() {
this.resources = [];
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.processEntry(entry);
}
});
}
start() {
try {
this.observer.observe({ type: 'resource', buffered: true });
} catch (e) {
// Fallback for older browsers
this.observer.observe({ entryTypes: ['resource'] });
}
}
processEntry(entry) {
const resource = {
name: entry.name,
type: this.getResourceType(entry),
duration: entry.duration,
transferSize: entry.transferSize,
decodedBodySize: entry.decodedBodySize,
timing: {
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
ssl:
entry.secureConnectionStart > 0
? entry.connectEnd - entry.secureConnectionStart
: 0,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart,
},
cached: entry.transferSize === 0 && entry.decodedBodySize > 0,
};
this.resources.push(resource);
}
getResourceType(entry) {
const initiatorType = entry.initiatorType;
const url = entry.name;
if (
initiatorType === 'img' ||
/\.(jpg|jpeg|png|gif|svg|webp)$/i.test(url)
) {
return 'image';
}
if (initiatorType === 'script' || /\.js$/i.test(url)) {
return 'script';
}
if (initiatorType === 'link' || /\.css$/i.test(url)) {
return 'stylesheet';
}
if (/\.(woff2?|ttf|eot)$/i.test(url)) {
return 'font';
}
if (initiatorType === 'fetch' || initiatorType === 'xmlhttprequest') {
return 'api';
}
return 'other';
}
getReport() {
const byType = {};
let totalSize = 0;
let totalDuration = 0;
this.resources.forEach((resource) => {
if (!byType[resource.type]) {
byType[resource.type] = {
count: 0,
totalSize: 0,
totalDuration: 0,
cachedCount: 0,
};
}
byType[resource.type].count++;
byType[resource.type].totalSize += resource.transferSize || 0;
byType[resource.type].totalDuration += resource.duration;
if (resource.cached) byType[resource.type].cachedCount++;
totalSize += resource.transferSize || 0;
totalDuration += resource.duration;
});
return {
totalResources: this.resources.length,
totalSize: this.formatBytes(totalSize),
byType: Object.entries(byType).map(([type, data]) => ({
type,
count: data.count,
size: this.formatBytes(data.totalSize),
avgDuration: (data.totalDuration / data.count).toFixed(2) + 'ms',
cacheHitRate: ((data.cachedCount / data.count) * 100).toFixed(1) + '%',
})),
slowest: this.resources
.sort((a, b) => b.duration - a.duration)
.slice(0, 5)
.map((r) => ({ name: r.name, duration: r.duration.toFixed(2) + 'ms' })),
largest: this.resources
.filter((r) => r.transferSize > 0)
.sort((a, b) => b.transferSize - a.transferSize)
.slice(0, 5)
.map((r) => ({ name: r.name, size: this.formatBytes(r.transferSize) })),
};
}
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
stop() {
this.observer.disconnect();
}
clear() {
this.resources = [];
}
}
// ============================================
// LONG TASK MONITORING
// ============================================
/**
* Monitor long tasks that block the main thread
*/
function monitorLongTasks(callback) {
let longTasks = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const task = {
startTime: entry.startTime,
duration: entry.duration,
attribution:
entry.attribution?.map((attr) => ({
name: attr.name,
containerType: attr.containerType,
containerId: attr.containerId,
containerSrc: attr.containerSrc,
})) || [],
};
longTasks.push(task);
if (callback) {
callback(task);
}
// Log warning for very long tasks
if (entry.duration > 100) {
console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`);
}
}
});
try {
observer.observe({ type: 'longtask', buffered: true });
} catch (e) {
console.warn('Long task monitoring not supported');
}
return {
getTasks: () => [...longTasks],
getStats: () => {
if (longTasks.length === 0) return null;
const durations = longTasks.map((t) => t.duration);
return {
count: longTasks.length,
total: durations.reduce((a, b) => a + b, 0).toFixed(2) + 'ms',
average:
(durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(2) +
'ms',
max: Math.max(...durations).toFixed(2) + 'ms',
min: Math.min(...durations).toFixed(2) + 'ms',
};
},
clear: () => {
longTasks = [];
},
disconnect: () => observer.disconnect(),
};
}
// ============================================
// NAVIGATION TIMING
// ============================================
/**
* Get detailed navigation timing
*/
function getNavigationTiming() {
const entry = performance.getEntriesByType('navigation')[0];
if (!entry) {
return null;
}
return {
// DNS
dnsLookup: entry.domainLookupEnd - entry.domainLookupStart,
// Connection
tcpConnect: entry.connectEnd - entry.connectStart,
sslHandshake:
entry.secureConnectionStart > 0
? entry.connectEnd - entry.secureConnectionStart
: 0,
// Request/Response
timeToFirstByte: entry.responseStart - entry.requestStart,
contentDownload: entry.responseEnd - entry.responseStart,
// DOM Processing
domInteractive: entry.domInteractive - entry.responseEnd,
domContentLoaded:
entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
domComplete: entry.domComplete - entry.responseEnd,
// Page Load
loadEvent: entry.loadEventEnd - entry.loadEventStart,
totalPageLoad: entry.loadEventEnd - entry.startTime,
// Transfer
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize,
// Type
type: entry.type, // navigate, reload, back_forward, prerender
redirectCount: entry.redirectCount,
};
}
// ============================================
// PERFORMANCE BUDGET MONITOR
// ============================================
/**
* Monitor against performance budgets
*/
class PerformanceBudget {
constructor(budgets = {}) {
this.budgets = {
LCP: budgets.LCP || 2500,
FID: budgets.FID || 100,
CLS: budgets.CLS || 0.1,
totalJS: budgets.totalJS || 300000, // 300KB
totalCSS: budgets.totalCSS || 50000, // 50KB
totalImages: budgets.totalImages || 500000, // 500KB
resourceCount: budgets.resourceCount || 50,
longTasks: budgets.longTasks || 5,
};
this.violations = [];
this.metrics = {};
}
start() {
// Monitor Web Vitals
monitorWebVitals((metric, value) => {
this.metrics[metric] = value;
this.checkBudget(metric, value);
});
// Monitor resources
this.resourceAnalyzer = new ResourceAnalyzer();
this.resourceAnalyzer.start();
// Monitor long tasks
this.longTaskMonitor = monitorLongTasks(() => {
const taskCount = this.longTaskMonitor.getTasks().length;
this.checkBudget('longTasks', taskCount);
});
// Periodic resource check
this.checkInterval = setInterval(() => {
this.checkResourceBudgets();
}, 5000);
}
checkBudget(metric, value) {
const budget = this.budgets[metric];
if (budget !== undefined && value > budget) {
const violation = {
metric,
value,
budget,
exceeded: value - budget,
timestamp: Date.now(),
};
this.violations.push(violation);
console.warn(
`Budget violation: ${metric} is ${value}, budget is ${budget}`
);
}
}
checkResourceBudgets() {
const resources = this.resourceAnalyzer.resources;
// Check by type
const byType = {
script: 0,
stylesheet: 0,
image: 0,
};
resources.forEach((r) => {
if (byType[r.type] !== undefined) {
byType[r.type] += r.transferSize || 0;
}
});
this.checkBudget('totalJS', byType.script);
this.checkBudget('totalCSS', byType.stylesheet);
this.checkBudget('totalImages', byType.image);
this.checkBudget('resourceCount', resources.length);
}
getReport() {
return {
budgets: this.budgets,
metrics: this.metrics,
violations: this.violations,
status: this.violations.length === 0 ? 'pass' : 'fail',
summary: Object.entries(this.budgets).map(([metric, budget]) => ({
metric,
budget,
current: this.metrics[metric],
status: (this.metrics[metric] || 0) <= budget ? 'pass' : 'fail',
})),
};
}
stop() {
clearInterval(this.checkInterval);
this.resourceAnalyzer.stop();
this.longTaskMonitor.disconnect();
}
}
// ============================================
// NODE.JS SIMULATION
// ============================================
console.log('=== PerformanceObserver Examples ===');
console.log('Note: PerformanceObserver is a browser API.');
console.log('');
// Simulate some concepts
console.log('Performance Entry Types:');
const entryTypes = [
'navigation - Page load timing',
'resource - Resource loading timing',
'mark - Custom timestamps',
'measure - Duration between marks',
'paint - First paint metrics',
'longtask - Main thread blocking',
'largest-contentful-paint - LCP metric',
'layout-shift - CLS metric',
'first-input - FID metric',
];
entryTypes.forEach((type) => console.log(` - ${type}`));
console.log('');
console.log('Core Web Vitals Thresholds:');
console.log(' LCP: Good ≤ 2.5s, Poor > 4.0s');
console.log(' FID: Good ≤ 100ms, Poor > 300ms');
console.log(' CLS: Good ≤ 0.1, Poor > 0.25');
console.log('');
// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
basicPerformanceObserver,
PerformanceTimer,
monitorWebVitals,
ResourceAnalyzer,
monitorLongTasks,
getNavigationTiming,
PerformanceBudget,
};
}