How to Make Your Site Faster with the Performance API

    Craig Buckler
    Share

    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.