Skip to content

Startup performance: paint and navigation timings

Published

In one line: Per MDN, the data PerformancePaintTiming provides “helps you minimize the time that users have to wait before they can see the site’s content start to appear”; PerformanceNavigationTiming separately “provides methods and properties to store and retrieve metrics regarding the browser’s document navigation events.” Together those two entry types are what this page measures with.

Per MDN, PerformancePaintTiming “provides timing information about ‘paint’ (also called ‘render’) operations during web page construction,” where “‘Paint’ refers to conversion of the render tree to on-screen pixels.” MDN names two key paint moments this API provides:

  • First Paint (FP) — “Time when anything is rendered.” MDN notes that “the marking of the first paint is optional, not all user agents report it.”
  • First Contentful Paint (FCP) — “Time when the first contentful paint — the first bit of DOM text or image content is rendered.”

Per MDN’s glossary, the FCP timestamp “indicates when the browser first rendered any text, image (including background images), video, canvas that had been drawn into, or non-empty SVG,” and it is “the first time users could start consuming page content.”

A third moment sits outside this interface: per MDN, Largest Contentful Paint (LCP) is provided by the LargestContentfulPaint API and is the “render time of the largest image or text block visible within the viewport.”

Per MDN, PerformanceObserver “is used to observe performance measurement events and be notified of new performance entries as they are recorded in the browser’s performance timeline.” MDN’s own example observes the paint entry type, using “the buffered option to access entries from before the observer creation”:

const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
// entry.name is either "first-paint" or "first-contentful-paint".
console.log(`The time to ${entry.name} was ${entry.startTime} milliseconds.`);
});
});
observer.observe({ type: 'paint', buffered: true });

Per MDN, a paint entry’s name “returns either first-paint or first-contentful-paint”, its startTime is “the timestamp when the paint occurred”, and its duration “returns 0” — so the timestamp, not the duration, is the number you want.

Per MDN, Performance.getEntriesByType() “only shows paint performance entries present in the browser’s performance timeline at the time you call this method.” That makes it the natural fallback when PerformanceObserver is unavailable:

function readPaintTimings() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) report(entry.name, entry.startTime);
});
observer.observe({ type: 'paint', buffered: true });
return;
}
// Fallback: no PerformanceObserver — read whatever is already on the timeline.
if (!('getEntriesByType' in performance)) return; // Nothing to read; skip reporting.
for (const entry of performance.getEntriesByType('paint')) {
report(entry.name, entry.startTime);
}
}

For the document milestones, PerformanceNavigationTiming is a single entry: per MDN, “only the current document is included in the performance timeline, so there is only one PerformanceNavigationTiming object in the performance timeline.”

const [navigation] = performance.getEntriesByType('navigation');
if (navigation) {
// domInteractive: immediately before readyState is set to "interactive".
// loadEventEnd: immediately after the load event handler completes.
console.log(navigation.domInteractive, navigation.loadEventEnd);
} else {
console.log('No navigation entry on this timeline.');
}

Per MDN, both interfaces are Baseline widely available: MDN describes each as a feature that “is well established and works across many devices and browser versions,” and dates PerformancePaintTiming as “available across browsers since April 2021” and PerformanceNavigationTiming as “available across browsers since October 2021.” MDN’s compatibility data puts paint timing in Chrome 60, Edge 79, Firefox 84, Safari 14.1 and Safari on iOS 14.5, and navigation timing in Chrome 57, Edge 12, Firefox 58, Safari 15 and Safari on iOS 15.1 — Safari shipped last in both cases, which is what those two Baseline dates track. Neither interface is marked deprecated or experimental by MDN.

MDN attaches the same caveat to both Baseline statements — “some parts of this feature may have varying levels of support” — and those exceptions are per-property, not per-interface. Within the paint interface, MDN states the split explicitly: paintTime “is broadly interoperable, whereas the presentationTime is implementation-dependent.” Several PerformanceNavigationTiming properties are marked experimental by MDN, including activationStart, confidence, criticalCHRestart and notRestoredReasons. So treat the interfaces as present and feature-detect at the property level, not the interface level.

  • Don’t require a first-paint entry. Per MDN, marking the first paint is optional and not all user agents report it — code that waits for first-paint before reporting may wait forever. Key your reporting on first-contentful-paint.
  • Register late and you lose the entries. getEntriesByType() returns only what is on the timeline when you call it, so an observer created after paint sees nothing unless you pass buffered: true.
  • Degrade through the paint timestamps, don’t assume the newest one. MDN’s own example checks presentationTime first, falls back to paintTime, and falls back again to loadTime in non-supporting browsers.
  • FCP is not “the layout finished”. Per MDN it excludes iframe content but includes text with pending webfonts, so an FCP can be reported while the final typeface is still loading.
  • Adjust prerendered timings against activationStart. Per MDN, activationStart represents “the time between when a document starts prerendering and when it is activated”, so raw timestamps from a prerendered document are not comparable to a normal navigation’s without accounting for it. MDN marks this property experimental.
  • Know what duration means here. Per MDN, a navigation entry’s startTime is 0 and its duration is the difference between loadEventEnd and startTime — a whole-document number, not a phase measurement.