Performance API
Master the browser Performance API — PerformanceObserver, Navigation Timing, Resource Timing, User Timing, Server Timing, and Element Timing — to build custom performance measurement, monitoring, and alerting into any web application.
When to Use
- You need to measure custom business metrics (time-to-interactive for specific components, checkout flow duration)
PerformanceObserver is needed to collect Web Vitals data (LCP, CLS, INP) in production
- Resource Timing data is needed to identify slow or large network requests
- Navigation Timing is needed to measure TTFB, DOM parsing, or total page load time
- Server Timing headers need to pass backend timing breakdowns to the frontend
- You are building a RUM (Real User Monitoring) pipeline to collect field performance data
performance.now() is preferred over Date.now() for sub-millisecond precision
- The
buffered: true flag is needed to capture entries that occurred before observer registration
- Long-lived SPAs need
performance.clearMarks() and performance.clearMeasures() to prevent memory accumulation
- Element Timing API is needed to measure render time of specific elements
Instructions
Use PerformanceObserver (not getEntriesByType). The observer pattern is more reliable — it captures entries as they occur and supports the buffered flag to retrieve entries that happened before registration:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.entryType, entry.name, entry.startTime, entry.duration);
}
});
// buffered: true captures entries that occurred before this line
observer.observe({ type: 'resource', buffered: true });
Measure custom business metrics with User Timing:
// Mark the start of an operation
performance.mark('checkout-start');
// ... checkout logic ...
// Mark the end
performance.mark('checkout-end');
// Measure the duration between marks
const measure = performance.measure('checkout-duration', 'checkout-start', 'checkout-end');
console.log('Checkout took:', measure.duration, 'ms');
// Measure with metadata (User Timing Level 3)
performance.measure('api-call', {
start: 'api-start',
end: 'api-end',
detail: { endpoint: '/api/cart', method: 'POST' },
});
Extract Navigation Timing data:
const nav = performance.getEntriesByType('navigation')[0];
const metrics = {
// DNS lookup
dns: nav.domainLookupEnd - nav.domainLookupStart,
// TCP connection
tcp: nav.connectEnd - nav.connectStart,
// TLS negotiation
tls: nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0,
// Time to First Byte
ttfb: nav.responseStart - nav.requestStart,
// HTML download
download: nav.responseEnd - nav.responseStart,
// DOM parsing
domParsing: nav.domInteractive - nav.responseEnd,
// DOM content loaded
domContentLoaded: nav.domContentLoadedEventEnd - nav.domContentLoadedEventStart,
// Total page load
pageLoad: nav.loadEventEnd - nav.startTime,
};
Analyze resource loading with Resource Timing:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// transferSize = bytes over the network (0 means cache hit)
// encodedBodySize = compressed size
// decodedBodySize = uncompressed size
if (entry.transferSize === 0) {
console.log('Cache hit:', entry.name);
} else {
const compressionRatio = entry.decodedBodySize / entry.encodedBodySize;
console.log(
'Resource:',
entry.name,
'Size:',
entry.transferSize,
'Compression:',
compressionRatio.toFixed(2)
);
}
}
});
observer.observe({ type: 'resource', buffered: true });
Read Server Timing from response headers:
// Server sends: Server-Timing: db;dur=53, cache;desc="Cache Read";dur=2, app;dur=120
const resources = performance.getEntriesByType('resource');
for (const resource of resources) {
if (resource.serverTiming) {
for (const timing of resource.serverTiming) {
console.log(`${timing.name}: ${timing.duration}ms (${timing.description})`);
// db: 53ms, cache: 2ms (Cache Read), app: 120ms
}
}
}
Use Element Timing for specific element render time:
<!-- Add elementtiming attribute to elements you want to measure -->
<img src="/hero.jpg" elementtiming="hero-image" alt="Hero" />
<h1 elementtiming="main-heading">Page Title</h1>
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.identifier, 'rendered at:', entry.startTime, 'ms');
}
});
observer.observe({ type: 'element', buffered: true });
Use performance.now() for high-resolution timing:
// performance.now() — microsecond precision, monotonic (not affected by clock adjustments)
const start = performance.now();
doExpensiveWork();
const elapsed = performance.now() - start;
console.log(`Elapsed: ${elapsed.toFixed(3)}ms`);
// Date.now() — millisecond precision, wall clock (affected by NTP, manual adjustments)
// DO NOT use Date.now() for performance measurement
Details
PerformanceEntry Types Reference
| Entry Type |
Source |
Key Properties |
navigation |
Page load |
responseStart, domInteractive, loadEventEnd |
resource |
Each network request |
transferSize, encodedBodySize, serverTiming |
mark |
performance.mark() |
name, startTime |
measure |
performance.measure() |
name, duration, detail |
longtask |
Tasks >50ms |
duration, attribution |
event |
User interactions |
processingStart, processingEnd, duration |
largest-contentful-paint |
LCP candidate |
element, url, size, startTime |
layout-shift |
Visual shifts |
value, hadRecentInput, sources |
element |
Elements with elementtiming |
identifier, startTime, element |
paint |
FP and FCP |
name (first-paint or first-contentful-paint) |
Worked Example: Etsy Product Card Timing
Etsy measures time-to-render for each product card on search results pages. They place performance.mark('card-render-start') before hydrating each card and performance.mark('card-render-end') after:
function renderProductCard(card, index) {
performance.mark(`card-${index}-start`);
hydrateCard(card);
performance.mark(`card-${index}-end`);
performance.measure(`card-${index}-render`, `card-${index}-start`, `card-${index}-end`);
}
// Aggregate and send to analytics
const measures = performance
.getEntriesByType('measure')
.filter((m) => m.name.includes('card'))
.map((m) => m.duration);
const p50 = percentile(measures, 50);
const p95 = percentile(measures, 95);
sendToGrafana({ cardRenderP50: p50, cardRenderP95: p95 });
Worked Example: Cloudflare Server Timing
Cloudflare uses Server-Timing headers to pass backend timing breakdowns through to the browser. The edge server adds headers: Server-Timing: edge;dur=2, origin;dur=150, db;dur=53. The frontend Performance API reads these without any custom telemetry:
const pageNav = performance.getEntriesByType('navigation')[0];
if (pageNav.serverTiming) {
const timingMap = Object.fromEntries(pageNav.serverTiming.map((t) => [t.name, t.duration]));
// { edge: 2, origin: 150, db: 53 }
dashboard.update(timingMap);
}
This gives frontend dashboards full-stack timing visibility: TTFB = 200ms, of which edge processing = 2ms, origin fetch = 150ms, database = 53ms.
Cross-Origin Timing Restrictions
By default, Resource Timing entries for cross-origin resources have zero values for detailed timing (DNS, TCP, TLS, request/response). This is a privacy protection. To enable full timing:
- The cross-origin server must include
Timing-Allow-Origin: * (or the specific origin) in response headers
- Without this header, only
startTime, duration, transferSize (sometimes 0), and encodedBodySize (0) are available
Anti-Patterns
Polling performance.getEntriesByType() instead of using PerformanceObserver. Polling wastes CPU, misses entries between polls, and does not capture entries that occur after the poll. PerformanceObserver fires exactly when entries are available.
Forgetting buffered: true on observer. Without buffered: true, entries that occurred before observer registration are missed. For LCP, CLS, and navigation timing, these entries always occur before your observer code runs.
Not clearing marks and measures in SPAs. In long-lived single-page applications, marks and measures accumulate in the performance buffer. Without performance.clearMarks() and performance.clearMeasures(), memory grows linearly with user actions. Clear after sending data to analytics.
Using Date.now() for performance measurement. Date.now() has 1ms resolution, is affected by system clock adjustments (NTP sync, manual changes), and can go backward. performance.now() has 5-microsecond resolution (subject to cross-origin isolation), is monotonic, and is unaffected by clock adjustments.
Measuring in non-isolated contexts expecting full precision. Without cross-origin isolation (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp), performance.now() is rounded to 100 microseconds (not 5 microseconds). For sub-millisecond measurements, enable cross-origin isolation.
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.
- Custom performance metrics are measured with User Timing API and sent to analytics.
- PerformanceObserver is used with
buffered: true for all metric collection.
1---2name: perf-performance-api3description: Performance API4---5# Performance API67> Master the browser Performance API — PerformanceObserver, Navigation Timing, Resource Timing, User Timing, Server Timing, and Element Timing — to build custom performance measurement, monitoring, and alerting into any web application.89## When to Use1011- You need to measure custom business metrics (time-to-interactive for specific components, checkout flow duration)12- `PerformanceObserver` is needed to collect Web Vitals data (LCP, CLS, INP) in production13- Resource Timing data is needed to identify slow or large network requests14- Navigation Timing is needed to measure TTFB, DOM parsing, or total page load time15- Server Timing headers need to pass backend timing breakdowns to the frontend16- You are building a RUM (Real User Monitoring) pipeline to collect field performance data17- `performance.now()` is preferred over `Date.now()` for sub-millisecond precision18- The `buffered: true` flag is needed to capture entries that occurred before observer registration19- Long-lived SPAs need `performance.clearMarks()` and `performance.clearMeasures()` to prevent memory accumulation20- Element Timing API is needed to measure render time of specific elements2122## Instructions23241. **Use `PerformanceObserver` (not `getEntriesByType`).** The observer pattern is more reliable — it captures entries as they occur and supports the `buffered` flag to retrieve entries that happened before registration:2526 ```javascript27 const observer = new PerformanceObserver((list) => {28 for (const entry of list.getEntries()) {29 console.log(entry.entryType, entry.name, entry.startTime, entry.duration);30 }31 });3233 // buffered: true captures entries that occurred before this line34 observer.observe({ type: 'resource', buffered: true });35 ```36372. **Measure custom business metrics with User Timing:**3839 ```javascript40 // Mark the start of an operation41 performance.mark('checkout-start');4243 // ... checkout logic ...4445 // Mark the end46 performance.mark('checkout-end');4748 // Measure the duration between marks49 const measure = performance.measure('checkout-duration', 'checkout-start', 'checkout-end');50 console.log('Checkout took:', measure.duration, 'ms');5152 // Measure with metadata (User Timing Level 3)53 performance.measure('api-call', {54 start: 'api-start',55 end: 'api-end',56 detail: { endpoint: '/api/cart', method: 'POST' },57 });58 ```59603. **Extract Navigation Timing data:**6162 ```javascript63 const nav = performance.getEntriesByType('navigation')[0];6465 const metrics = {66 // DNS lookup67 dns: nav.domainLookupEnd - nav.domainLookupStart,68 // TCP connection69 tcp: nav.connectEnd - nav.connectStart,70 // TLS negotiation71 tls: nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0,72 // Time to First Byte73 ttfb: nav.responseStart - nav.requestStart,74 // HTML download75 download: nav.responseEnd - nav.responseStart,76 // DOM parsing77 domParsing: nav.domInteractive - nav.responseEnd,78 // DOM content loaded79 domContentLoaded: nav.domContentLoadedEventEnd - nav.domContentLoadedEventStart,80 // Total page load81 pageLoad: nav.loadEventEnd - nav.startTime,82 };83 ```84854. **Analyze resource loading with Resource Timing:**8687 ```javascript88 const observer = new PerformanceObserver((list) => {89 for (const entry of list.getEntries()) {90 // transferSize = bytes over the network (0 means cache hit)91 // encodedBodySize = compressed size92 // decodedBodySize = uncompressed size93 if (entry.transferSize === 0) {94 console.log('Cache hit:', entry.name);95 } else {96 const compressionRatio = entry.decodedBodySize / entry.encodedBodySize;97 console.log(98 'Resource:',99 entry.name,100 'Size:',101 entry.transferSize,102 'Compression:',103 compressionRatio.toFixed(2)104 );105 }106 }107 });108 observer.observe({ type: 'resource', buffered: true });109 ```1101115. **Read Server Timing from response headers:**112113 ```javascript114 // Server sends: Server-Timing: db;dur=53, cache;desc="Cache Read";dur=2, app;dur=120115 const resources = performance.getEntriesByType('resource');116 for (const resource of resources) {117 if (resource.serverTiming) {118 for (const timing of resource.serverTiming) {119 console.log(`${timing.name}: ${timing.duration}ms (${timing.description})`);120 // db: 53ms, cache: 2ms (Cache Read), app: 120ms121 }122 }123 }124 ```1251266. **Use Element Timing for specific element render time:**127128 ```html129 <!-- Add elementtiming attribute to elements you want to measure -->130 <img src="/hero.jpg" elementtiming="hero-image" alt="Hero" />131 <h1 elementtiming="main-heading">Page Title</h1>132 ```133134 ```javascript135 const observer = new PerformanceObserver((list) => {136 for (const entry of list.getEntries()) {137 console.log(entry.identifier, 'rendered at:', entry.startTime, 'ms');138 }139 });140 observer.observe({ type: 'element', buffered: true });141 ```1421437. **Use `performance.now()` for high-resolution timing:**144145 ```javascript146 // performance.now() — microsecond precision, monotonic (not affected by clock adjustments)147 const start = performance.now();148 doExpensiveWork();149 const elapsed = performance.now() - start;150 console.log(`Elapsed: ${elapsed.toFixed(3)}ms`);151152 // Date.now() — millisecond precision, wall clock (affected by NTP, manual adjustments)153 // DO NOT use Date.now() for performance measurement154 ```155156## Details157158### PerformanceEntry Types Reference159160| Entry Type | Source | Key Properties |161| -------------------------- | ----------------------------- | -------------------------------------------------- |162| `navigation` | Page load | `responseStart`, `domInteractive`, `loadEventEnd` |163| `resource` | Each network request | `transferSize`, `encodedBodySize`, `serverTiming` |164| `mark` | `performance.mark()` | `name`, `startTime` |165| `measure` | `performance.measure()` | `name`, `duration`, `detail` |166| `longtask` | Tasks >50ms | `duration`, `attribution` |167| `event` | User interactions | `processingStart`, `processingEnd`, `duration` |168| `largest-contentful-paint` | LCP candidate | `element`, `url`, `size`, `startTime` |169| `layout-shift` | Visual shifts | `value`, `hadRecentInput`, `sources` |170| `element` | Elements with `elementtiming` | `identifier`, `startTime`, `element` |171| `paint` | FP and FCP | `name` (`first-paint` or `first-contentful-paint`) |172173### Worked Example: Etsy Product Card Timing174175Etsy measures time-to-render for each product card on search results pages. They place `performance.mark('card-render-start')` before hydrating each card and `performance.mark('card-render-end')` after:176177```javascript178function renderProductCard(card, index) {179 performance.mark(`card-${index}-start`);180 hydrateCard(card);181 performance.mark(`card-${index}-end`);182 performance.measure(`card-${index}-render`, `card-${index}-start`, `card-${index}-end`);183}184185// Aggregate and send to analytics186const measures = performance187 .getEntriesByType('measure')188 .filter((m) => m.name.includes('card'))189 .map((m) => m.duration);190const p50 = percentile(measures, 50);191const p95 = percentile(measures, 95);192sendToGrafana({ cardRenderP50: p50, cardRenderP95: p95 });193```194195### Worked Example: Cloudflare Server Timing196197Cloudflare uses `Server-Timing` headers to pass backend timing breakdowns through to the browser. The edge server adds headers: `Server-Timing: edge;dur=2, origin;dur=150, db;dur=53`. The frontend Performance API reads these without any custom telemetry:198199```javascript200const pageNav = performance.getEntriesByType('navigation')[0];201if (pageNav.serverTiming) {202 const timingMap = Object.fromEntries(pageNav.serverTiming.map((t) => [t.name, t.duration]));203 // { edge: 2, origin: 150, db: 53 }204 dashboard.update(timingMap);205}206```207208This gives frontend dashboards full-stack timing visibility: TTFB = 200ms, of which edge processing = 2ms, origin fetch = 150ms, database = 53ms.209210### Cross-Origin Timing Restrictions211212By default, Resource Timing entries for cross-origin resources have zero values for detailed timing (DNS, TCP, TLS, request/response). This is a privacy protection. To enable full timing:2132141. The cross-origin server must include `Timing-Allow-Origin: *` (or the specific origin) in response headers2152. Without this header, only `startTime`, `duration`, `transferSize` (sometimes 0), and `encodedBodySize` (0) are available216217### Anti-Patterns218219**Polling `performance.getEntriesByType()` instead of using `PerformanceObserver`.** Polling wastes CPU, misses entries between polls, and does not capture entries that occur after the poll. `PerformanceObserver` fires exactly when entries are available.220221**Forgetting `buffered: true` on observer.** Without `buffered: true`, entries that occurred before observer registration are missed. For LCP, CLS, and navigation timing, these entries always occur before your observer code runs.222223**Not clearing marks and measures in SPAs.** In long-lived single-page applications, marks and measures accumulate in the performance buffer. Without `performance.clearMarks()` and `performance.clearMeasures()`, memory grows linearly with user actions. Clear after sending data to analytics.224225**Using `Date.now()` for performance measurement.** `Date.now()` has 1ms resolution, is affected by system clock adjustments (NTP sync, manual changes), and can go backward. `performance.now()` has 5-microsecond resolution (subject to cross-origin isolation), is monotonic, and is unaffected by clock adjustments.226227**Measuring in non-isolated contexts expecting full precision.** Without cross-origin isolation (`Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`), `performance.now()` is rounded to 100 microseconds (not 5 microseconds). For sub-millisecond measurements, enable cross-origin isolation.228229## Source230231- W3C Performance Timeline Level 2 — https://www.w3.org/TR/performance-timeline/232- W3C User Timing Level 3 — https://www.w3.org/TR/user-timing/233- W3C Resource Timing Level 2 — https://www.w3.org/TR/resource-timing-2/234- W3C Navigation Timing Level 2 — https://www.w3.org/TR/navigation-timing-2/235- MDN Performance API reference — https://developer.mozilla.org/en-US/docs/Web/API/Performance_API236237## Process2382391. Read the instructions and examples in this document.2402. Apply the patterns to your implementation, adapting to your specific context.2413. Verify your implementation against the details and edge cases listed above.242243## Harness Integration244245- **Type:** knowledge — this skill is a reference document, not a procedural workflow.246- **No tools or state** — consumed as context by other skills and agents.247248## Success Criteria249250- The patterns described in this document are applied correctly in the implementation.251- Edge cases and anti-patterns listed in this document are avoided.252- Custom performance metrics are measured with User Timing API and sent to analytics.253- PerformanceObserver is used with `buffered: true` for all metric collection.