How to Make Your Site Faster with the Performance API – SitePoint

This tutorial explains how to use the Performance API to record DevTool-like statistics from real users accessing your application.
Assessing web application performance using browser DevTools is useful, but it’s not easy to replicate real-world usage. People in different locations using different devices, browsers, and networks will all have differing experiences.
An Introduction to the Performance API
The Performance API uses a buffer to record DevTool-like metrics in object properties at certain points in the lifetime of your web page. Those points include:
Page navigation: record page load redirects, connections, handshakes, DOM events, and more.
Resource loading: record asset loading such as images, CSS, scripts, and Ajax calls.
Paint metrics: record browser rendering information.
Custom performance: record arbitrary application processing times to find slow functions.
All the APIs are available in client-side JavaScript, including Web Workers. You can detect API support using:
if (‘performance’ in window) {

}

Note: be aware that Safari doesn’t support all methods, despite implementing most of the API.
The custom (user) performance APIs are also replicated in:
Isn’t Date() Good Enough?
You may have seen examples using the Date() function to record elapsed times. For example:
const start = new Date();

const elapsed = new Date() – start;

However, Date() calculations are limited to the closest millisecond and based on the system time, which can be updated by the OS at any point.
The Performance API uses a separate, higher-resolution timer that can record in fractions of a millisecond. It also offers metrics that would be impossible to record otherwise, such as redirect and DNS lookup timings.
Recording Performance Metrics
Calculating performance metrics in client-side code is useful if you can record it somewhere. You can send statistics to your server for analysis using Ajax Fetch / XMLHttpRequest requests or the Beacon API.
Alternatively, most analytic systems offer custom event-like APIs to record timings. For example, the Google Analytics User Timings API can record the time to DOMContentLoaded by passing a category (‘pageload’), variable name (“DOMready”), and a value:
const pageload = performance.getEntriesByType( ‘navigation’ )[0];

ga(‘send’, ‘timing’, ‘pageload’, ‘DOMready’, pageload.domContentLoadedEventStart);

This example uses the Page Navigation Timing API. so let’s start there …
Page Navigation Timing
Testing your site on a fast connection is unlikely to be indicative of user experience. The browser DevTools Network tab allows you to throttle speeds, but it can’t emulate poor or intermittent 3G signals.
The Navigation Timing API pushes a single PerformanceNavigationTiming object to the performance buffer. It contains information about redirects, load times, file sizes, DOM events, and so on, observed by a real user.
Access the object by running:
const pagePerf = performance.getEntriesByType( ‘navigation’ );

Or access it by passing the page URL (window.location) to the getEntriesByName() method:
const pagePerf = performance.getEntriesByName( window.location );

Both return an array with a single element containing an object with read-only properties. For example:
[
{
name: “https://site.com/”,
initiatorType: “navigation”,
entryType: “navigation”,
initiatorType: “navigation”,
type: “navigate”,
nextHopProtocol: “h2”,
startTime: 0

}
]

The object includes resource identification properties:
property
description
name
the resource URL
entryType
performance type — “navigation” for a page, “resource” for an asset
initiatorType
resource which initiated the download — “navigation” for a page
nextHopProtocol
network protocol
serverTiming
array of PerformanceServerTiming objects
Note: performanceServerTiming name, description, and duration metrics are written to the HTTP Server-Timing header by the server response.
The object includes resource timing properties in milliseconds relative to the start of the page load. Timings would normally be expected in this order:
property
description
startTime
timestamp when fetch started — 0 for a page
workerStart
timestamp before starting the Service Worker
redirectStart
timestamp of the first redirect
redirectEnd
timestamp after receiving the last byte of the last redirect
fetchStart
timestamp before the resource fetch
domainLookupStart
timestamp before the DNS lookup
domainLookupEnd
timestamp after the DNS lookup
connectStart
timestamp before establishing a server connection
connectEnd
timestamp after establishing a server connection
secureConnectionStart
timestamp before the SSL handshake
requestStart
timestamp before the browser request
responseStart
timestamp when the browser receives the first byte of data
responseEnd
timestamp after receiving the last byte of data
duration
the time elapsed between startTime and responseEnd
The object includes download size properties in bytes:
property
description
transferSize
the resource size, including the header and body
encodedBodySize
the resource body size before decompressing
decodedBodySize
the resource body size after decompressing
Finally, the object includes further navigation and DOM event properties (not available in Safari):
property
description
type
either “navigate”, “reload”, “back_forward” or “prerender”
redirectCount
number of redirects
unloadEventStart
timestamp before the unload event of the previous document
unloadEventEnd
timestamp after the unload event of the previous document
domInteractive
timestamp when HTML parsing and DOM construction is complete
domContentLoadedEventStart
timestamp before running DOMContentLoaded event handlers
domContentLoadedEventEnd
timestamp after running DOMContentLoaded event handlers
domComplete
timestamp when DOM construction and DOMContentLoaded events have completed
loadEventStart
timestamp before the page load event has fired
loadEventEnd
timestamp after the page load event. All assets are downloaded
Example to record page loading metrics after the page has fully loaded:
‘performance’ in window && window.addEventListener(‘load’, () => {

const
pagePerf = performance.getEntriesByName( window.location )[0],
pageDownload = pagePerf.duration,
pageDomComplete = pagePerf.domComplete;

});

The Resource Timing API pushes a PerformanceResourceTiming object to the performance buffer whenever an asset such as an image, font, CSS file, JavaScript file, or any other item is loaded by the page. Run:
const resPerf = performance.getEntriesByType( ‘resource’ );

This returns an array of resource timing objects. These have the same properties as the page timing shown above, but without the navigation and DOM event information.
Here’s an example result:
[
{
name: “https://site.com/style.css”,
entryType: “resource”,
initiatorType: “link”,
fetchStart: 150,
duration: 300

},
{
name: “https://site.com/script.js”,
entryType: “resource”,
initiatorType: “script”,
fetchStart: 302,
duration: 112

},

]

A single resource can be examined by passing its URL to the .getEntriesByName() method:
const resourceTime = performance.getEntriesByName(‘https://site.com/style.css’);

This returns an array with a single element:
[
{
name: “https://site.com/style.css”,
entryType: “resource”,
initiatorType: “link”,
fetchStart: 150,
duration: 300

}
]

You could use the API to report the load time and decompressed size of each CSS file:

const css = performance.getEntriesByType(‘resource’)
.filter( r => r.initiatorType === ‘link’ && r.name.includes(‘.css’))
.map( r => ({

name: r.name,
load: r.duration + ‘ms’,
size: r.decodedBodySize + ‘ bytes’

}) );

The css array now contains an object for each CSS file. For example:
[
{
name: “https://site.com/main.css”,
load: “155ms”,
size: “14304 bytes”
},
{
name: “https://site.com/grid.css”,
load: “203ms”,
size: “5696 bytes”
}
]

Note: a load and size of zero indicates the asset was already cached.
At least 150 resource metric objects will be recorded to the performance buffer. You can define a specific number with the .setResourceTimingBufferSize(N) method. For example:

performance.setResourceTimingBufferSize(500);

Existing metrics can be cleared with the .clearResourceTimings() method.
Browser Paint Timing
First Contentful Paint (FCP) measures how long it takes to render content after the user navigates to your page. The Performance section of Chrome’s DevTool Lighthouse panel shows the metric. Google considers FCP times of less than two seconds to be good and your page will appear faster than 75% of the Web.
The Paint Timing API pushes two records two PerformancePaintTiming objects to the performance buffer when:
first-paint occurs: the browser paints the first pixel, and
first-contentful-paint occurs: the browser paints the first item of DOM content
Both objects are returned in an array when running:
const paintPerf = performance.getEntriesByType( ‘paint’ );

Example result:
[
{
“name”: “first-paint”,
“entryType”: “paint”,
“startTime”: 125
},
{
“name”: “first-contentful-paint”,
“entryType”: “paint”,
“startTime”: 127
}
]

The startTime is relative to the initial page load.
User Timing
The Performance API can be used to time your own application functions. All user timing methods are available in client-side JavaScript, Web Workers, Deno, and Node.js.
Note that Node.js scripts must load the Performance hooks (perf_hooks) module.
CommonJS require syntax:
const { performance } = require(‘perf_hooks’);

Or ES module import syntax:
import { performance } from ‘perf_hooks’;

The easiest option is performance.now(), which returns a high-resolution timestamp from the beginning of the process’s lifetime.
You can use performance.now() for simple timers. For example:
const start = performance.now();

const elapsed = performance.now() – start;

Note: a non-standard timeOrigin property returns a timestamp in Unix time. It can be used in Node.js and browser JavaScript, but not in IE and Safari.
performance.now() quickly becomes impractical when managing multiple timers. The .mark() method adds a named PerformanceMark object object to the performance buffer. For example:
performance.mark(‘script:start’);

performance.mark(‘p1:start’);

performance.mark(‘p1:end’);

performance.mark(‘p2:start’);

performance.mark(‘p2:end’);

performance.mark(‘script:end’);

The following code returns an array of mark objects:
const marks = performance.getEntriesByType( ‘mark’ );

with entryType, name, and startTime properties:
[
{
entryType: “mark”,
name: “script:start”,
startTime: 100
},
{
entryType: “mark”,
name: “p1:start”,
startTime: 200
},
{
entryType: “mark”,
name: “p1:end”,
startTime: 300
},

]

The elapsed time between two marks can be calculated using the .measure() method. It’s passed a measure name, the start mark name (or null to use zero), and the end mark name (or null to use the current time):
performance.measure(‘p1’, ‘p1:start’, ‘p1:end’);
performance.measure(‘script’, null, ‘script:end’);

Each call pushes a PerformanceMeasure object with a calculated duration to the performance buffer. An array of measures can be accessed by running:
const measures = performance.getEntriesByType( ‘measure’ );

Example:
[
{
entryType: “measure”,
name: “p1”,
startTime: 200,
duration: 100
},
{

entryType: “measure”,
name: “script”,
startTime: 0,
duration: 500
}
]

Mark or measure objects can be retrieved by name using the .getEntriesByName() method:
performance.getEntriesByName( ‘p1’ );

Other methods:
A PerformanceObserver can watch for changes to the buffer and run a function when specific objects appear. An observer function is defined with two parameters:
list: the observer entries
observer (optional): the observer object
function performanceHandler(list, observer) {

list.getEntries().forEach(entry => {

console.log(`name : ${ entry.name }`);
console.log(`type : ${ entry.type }`);
console.log(`duration: ${ entry.duration }`);

});

}

This function is passed to a new PerformanceObserver object. The .observe() method then sets observable entryTypes (generally “mark”, “measure”, and/or “resource”):
let observer = new PerformanceObserver( performanceHandler );
observer.observe( { entryTypes: [ ‘mark’, ‘measure’ ] } );

The performanceHandler() function will run whenever a new mark or measure object is pushed to the performance buffer.
Self-profiling API
The Self-profiling API is related to the Performance API and can help find inefficient or unnecessary background functions without having to manually set marks and measures.
Example code:

const profile = await performance.profile({ sampleInterval: 10 });

const trace = await profile.stop();

The trace returns data about what script, function, and line number was executing at every sampled interval. Repeated references to the same code could indicate that further optimization may be possible.
The API is currently under development (see Chrome Status) and subject to change.
Tuning Application Performance
The Performance API offers a way to measure website and application speed on actual devices used by real people in different locations on a range of connections. It makes it easy to collate DevTool-like metrics for everyone and identify potential bottlenecks.
Solving those performance problems is another matter, but the SitePoint Jump Start Web Performance book will help. It provides a range of quick snacks, simple recipes, and life-changing diets to make your site faster and more responsive.

Coded at

Share your love

Leave a Reply