Track performance of your web application with JavaScript

Read how to track the performance of your web application with JavaScript.

Track performance of your web application with JavaScript

I. Introduction ✌️

This September I had the chance to join Forest Admin, a company which does all the heavy lifting of building the admin panel of any web application and provides an API-based framework to implement all your specific business processes.

Photo from forestadmin.com

My first mission was to implement the monitoring of the application loading time as well as the monitoring of the time of the requests made by our customers on their admin backend.

The purpose of such a feature is to be able to target projects where some users encounter long loading times in order to optimize the configuration of their interface. This makes navigation and interaction on the application more fluid and thus improves the user experience.

To do this, the first step is to find out how we are going to implement such a feature. Depending on the development framework used, add-ons may already exist. If you are a React developer for example the react-addons-perf should allow you to do the tracking you want. At Forest Admin, we use Ember.js and there is no add-on similar to React.

We will therefore use Performance API which, according to the documentation, should allow us to do what we want to do:

The High Resolution Time standard defines a Performance interface that supports client-side latency measurements within applications. The Performance interfaces are considered high resolution because they are accurate to a thousandth of a millisecond (subject to hardware or software constraints).

II. Performance API 🚀

The Performance API benefits from many diverse and varied methods. In our case we will only need 5 different methods:

  • mark
  • measure
  • getEntriesByType
  • clearMarks / clearMeasures

1. mark

The mark method allows us to place a time marker. It takes only one argument (string) to reference the marker and returns nothing. This will allow us to calculate a time later.

performance.mark('start');

2. measure

The measure method allows to measure a time difference between two markers. It takes 3 arguments: the name of the created measure (string), the start marker (string), the end marker (string). This method returns an object with a duration property that calculates the difference between two markers.

async function timeDuration() {
  performance.mark('start');
  await new Promise(resolve => setTimeout(resolve, 100))
  performance.mark('end');

  return performance.measure('time', 'start', 'end').duration;
}

timeDuration().then((result) => console.log(result));
// output: 100

3. getEntriesByType

The method getEntriesByType allows to access all entities of a certain type created. It returns an array of objects and takes as argument the type of input (string) among the following: frame, navigation, resource, mark, measure, paint & longtask. Don’t worry, we will only need mark and measure. 😉

performance.getEntriesByType('measure');
// output: return an Array of Object containing all the measure

4. clearMarks / clearMeasures

The clearMarks / clearMeasures methods are used to remove previously added markers and measures from the browser cache. These methods do not return anything and do not take any arguments.

performance.mark('start');
performance.mark('end');
performance.measure('time_duration', 'start', 'end');

console.log(performance.getEntriesByType('mark').length);
// output: 2
console.log(performance.getEntriesByType('measure').length);
// output: 1

performance.clearMarks();
performance.clearMeasures();
console.log(performance.getEntriesByType('mark').length);
// output: 0
console.log(performance.getEntriesByType('measure').length);
// output: 0

III. Let’s dive into the code 🌲

Photo from pixabay, free to use.

Now that we know the methods to implement the functionality we need to integrate it into the existing code. We have two possibilities: create the markers and perform the measurements directly in the places in the desired code, or create a service (time-tracker.js) and inject it into the code. For the sake of clarity we will choose the second option.


export default Service.extend({

  clearMarks() {
    performance.clearMarks();
  },

  startInterface() {
    performance.mark('timing_interface_start');
  },

  stopInterface() {
    performance.mark('timing_interface_stop');
  },

  measureTimingInterface() {
    return performance.measure(
      'timing_interface',
      'timing_interface_start',
      'timing_interface_stop'
    );
  },

  startRequest() {
    performance.mark('timing_request_start');
  },

  stopRequest() {
    performance.mark('timing_request_stop');
  },

  measureTimingRequest() {
    return performance.measure(
      'timing_request',
      'timing_request_start',
      'timing_request_stop'
    );
  },
});
time-tracker.js

Simple, isn’t it? We now need to call the service functions at the places in the code that will allow us to track the request and interface loading times.

For the interface, the timing_interface_start marker must be called at the beginning of the page loading. The model method of the route being the first called we will choose to put it here. It is also possible to use the methods on the lifecycles of a component or any other function called initially.

However, care must be taken where the marker is placed. If the marker is not placed in the right place, the subsequent measurement will not be accurate.

import { inject as service } from '@ember/service';

export default class RouteExample extends Route {
  @service timeTracker;
  
  model() {
    this.timeTracker.startInterface();
    // do something
  }
}
route.js

For the timing_interface_stop marker it must be called when the application rendering just ended. As this concerns the rendering of several components, a logical way is to use the methods on the lifecycles of these components. The didRender ember component method seems to be a good candidate.

Then comes the measurement of the total loading time of the interface. It is possible to put it after the timing_interface_stop marker or at any other desired location.

import { inject as service } from '@ember/service';

export default class RenderingTracker extends Component {
  @service timeTracker;

  didRender() {
    this.timeTracker.stopInterface();
  }
  stopInterfaceTracking() {
    return this.timeTracker.measureTimingInterface()
  }
}
component.js

For the calculation of the request time on the user’s server it is enough to surround the function being in charge of requesting the user’s server with the markers timing_request_start and timing_request_stop.

And then we can measure the time of the request by calling the measureTimingRequest() method of our service.

import { inject as service } from '@ember/service';

export default class RouteExample extends Route {
  @service timeTracker;
  
  model() {
    this.timeTracker.startInterface();
    // do something
  }
  
  async function fetchData(params) {
    //do something
  
    this.timeTracker.startRequest();
    const records = await fetchRecords(params);
    this.timingTracker.stopRequest();
    
    const timingRequest = this.timingTracker.measureTimingRequest()
    // do something else

  }
}
route.js

Conclusion 🥳

I hope you enjoyed this article. Any comment is welcome and I will be happy to discuss it. I would like to take this opportunity to thank the people at Forest Admin who helped me to implement this feature.

There are other possibilities to implement this functionality. I chose to show you the way that makes more sense to me. API Performance is a powerful tool to gain knowledge about the performance of your application. It is important to know if users encounter difficulties when browsing your application. Indeed, it allows you to proactively target problems in order to solve them and improve the user experience.

A next step could be to integrate a follow up email to users whose loading times exceed a threshold to guide them on certain parameters to be optimized.