Performance · Concept
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.
The paint moments the browser reports
Section titled “The paint moments the browser reports”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.”
How to use it
Section titled “How to use it”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.
Detecting and falling back
Section titled “Detecting and falling back”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.');}Where it is supported
Section titled “Where it is supported”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.
Practical checklist
Section titled “Practical checklist”- 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-paintbefore reporting may wait forever. Key your reporting onfirst-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 passbuffered: true. - Degrade through the paint timestamps, don’t assume the newest one. MDN’s
own example checks
presentationTimefirst, falls back topaintTime, and falls back again toloadTimein 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,activationStartrepresents “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
durationmeans here. Per MDN, a navigation entry’sstartTimeis0and itsdurationis the difference betweenloadEventEndandstartTime— a whole-document number, not a phase measurement.
Where to go next
Section titled “Where to go next”- Core Web Vitals — where LCP and the other field metrics are covered.
- App shell architecture — a structural approach to what renders first.
- Back/forward cache (bfcache) — a restore is not
a fresh startup, and
notRestoredReasonsreports why one did not happen.
Specifications
| Specification | Status |
|---|---|
| None. | |