Geoff is a technical editor and writer at Smashing Magazine. On top of that, he’s an independent contractor who teaches front-end design and development classes …
More about
Geoff ↬
Email Newsletter
, who help optimize web performance to improve user experience. Thank you!
There’s quite a buzz in the performance community with the Interaction to Next Paint (INP) metric becoming an official (CWV) metric in a few short weeks. If you haven’t heard, INP is replacing the First Input Delay (FID) metric, something you can read all about here on Smashing Magazine as a guide to prepare for the change.
But that’s not what I really want to talk about. With performance at the forefront of my mind, I decided to head over to MDN for a fresh look at the Performance API. We can use it to report the load time of elements on the page, even going so far as to report on Core Web Vitals metrics in real time. Let’s look at a few ways we can use the API to report some CWV metrics.
Browser Support Warning
Before we get started, a quick word about browser support. The Performance API is huge in that it contains a lot of different interfaces, properties, and methods. While the majority of it is supported by all major browsers, Chromium-based browsers are the only ones that support all of the CWV properties. The only other is Firefox, which supports the First Contentful Paint (FCP) and Largest Contentful Paint (LCP) API properties.So, we’re looking at a feature of features, as it were, where some are well-established, and others are still in the experimental phase. But as far as Core Web Vitals go, we’re going to want to work in Chrome for the most part as we go along.
First, We Need Data Access
There are two main ways to retrieve the performance metrics we care about:
Using the performance.getEntries() method, or
Using a PerformanceObserver instance.
Using a PerformanceObserver instance offers a few important advantages:
PerformanceObserver observes performance metrics and dispatches them over time. Instead, using performance.getEntries() will always return the entire list of entries since the performance metrics started being recorded.
PerformanceObserver dispatches the metrics asynchronously, which means they don’t have to block what the browser is doing.
The element performance metric type doesn’t work with the performance.getEntries() method anyway.
That all said, let’s create a PerformanceObserver:
const lcpObserver = new PerformanceObserver(list => {});
For now, we’re passing an empty callback function to the PerformanceObserver constructor. Later on, we’ll change it so that it actually does something with the observed performance metrics. For now, let’s start observing:
The first very important thing in that snippet is the buffered: true property. Setting this to true means that we not only get to observe performance metrics being dispatched after we start observing, but we also want to get the performance metrics that were queued by the browser before we started observing.
The second very important thing to note is that we’re working with the largest-contentful-paint property. That’s what’s cool about the Performance API: it can be used to measure very specific things but also supports properties that are mapped directly to CWV metrics. We’ll start with the LCP metric before looking at other CWV metrics.
Reporting The Largest Contentful Paint
The largest-contentful-paint property looks at everything on the page, identifying the biggest piece of content on the initial view and how long it takes to load. In other words, we’re observing the full page load and getting stats on the largest piece of content rendered in view.
We already have our Performance Observer and callback:
Let’s fill in that empty callback so that it returns a list of entries once performance measurement starts:
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {// Returns the entire list of entriesconst entries = list.getEntries();});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
Next, we want to know which element is pegged as the LCP. It’s worth noting that the element representing the LCP is always the last element in the ordered list of entries. So, we can look at the list of returned entries and return the last one:
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {
// Returns the entire list of entries
const entries = list.getEntries();// The element representing the LCPconst el = entries[entries.length - 1];});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
The last thing is to display the results! We could create some sort of dashboard UI that consumes all the data and renders it in an aesthetically pleasing way. Let’s simply log the results to the console rather than switch gears.
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {
// Returns the entire list of entries
const entries = list.getEntries();
// The element representing the LCP
const el = entries[entries.length - 1];// Log the results in the consoleconsole.log(el.element);});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
There we go!It’s certainly nice knowing which element is the largest. But I’d like to know more about it, say, how long it took for the LCP to render:
// The Performance Observer
const lcpObserver = new PerformanceObserver(list => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1];
entries.forEach(entry => {
// Log the results in the console
console.log(
`The LCP is:`,
lcp.element,
`The time to render was ${entry.startTime} milliseconds.`,
);
});
});
// Call the Observer
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
// The LCP is:
// <h2 class="author-post__title mt-5 text-5xl">…</h2>
// The time to render was 832.6999999880791 milliseconds.
Reporting First Contentful Paint
This is all about the time it takes for the very first piece of DOM to get painted on the screen. Faster is better, of course, but the way Lighthouse reports it, a “passing” score comes in between 0 and 1.8 seconds.
Just like we set the type property to largest-contentful-paint to fetch performance data in the last section, we’re going to set a different type this time around: paint.
When we call paint, we tap into the PerformancePaintTiming interface that opens up reporting on first paint and first contentful paint.
// The Performance Observer
const paintObserver = new PerformanceObserver(list => {
const entries = list.getEntries();
entries.forEach(entry => {
// Log the results in the console.
console.log(
`The time to ${entry.name} took ${entry.startTime} milliseconds.`,
);
});
});
// Call the Observer.
paintObserver.observe({ type: "paint", buffered: true });
// The time to first-paint took 509.29999999981374 milliseconds.
// The time to first-contentful-paint took 509.29999999981374 milliseconds.