Skip to main content

How to Make Your Site Faster with the Performance API

By Craig Buckler

JavaScript

Share:

Free JavaScript Book!

Write powerful, clean and maintainable JavaScript.

RRP $11.95

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:

  1. Page navigation: record page load redirects, connections, handshakes, DOM events, and more.
  2. Resource loading: record asset loading such as images, CSS, scripts, and Ajax calls.
  3. Paint metrics: record browser rendering information.
  4. 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) {

  // call Performance APIs

}

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();

// ... run code ...

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 …

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;

});

Page Resource Timing

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:

// array of CSS files, load times, and file sizes
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:

// record 500 resources
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();

// ... run code ...

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');
// ... run process 1 ...
performance.mark('p1:end');

performance.mark('p2:start');
// ... run process 2 ...
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:

  1. list: the observer entries
  2. 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 }`);

    // other code, e.g.
    // send data via an Ajax request

  });

}

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:

// new profiler, 10ms sample rate
const profile = await performance.profile({ sampleInterval: 10 });

// ... run code ...

// stop profiler, get trace
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.

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

New books out now!

Learn the basics fo programming with the web's most popular language - JavaScript


A practical guide to leading radical innovation and growth.

Integromat Tower Ad