Measuring what matters with the User Timing API
Building web applications that hit 60 FPS means budgeting roughly 16ms per frame, which leaves little room for hidden inefficiencies. The User Timing API answers a narrower question than network or navigation timing tools: where, exactly, is your JavaScript spending its time? By inserting lightweight API calls into your code, you can collect high-resolution timestamps and calculate elapsed intervals between them.
High-resolution clocks
Traditional timing based on milliseconds is too coarse for meaningful analysis of modern web work. The High Resolution Time specification addresses this with DOMHighResTimeStamp, a floating-point type that reports time in milliseconds with sub-millisecond precision, potentially down to microseconds.
To read the current high-resolution time, call performance.now(). This method extends the Performance interface and returns the time elapsed since navigationStart from the PerformanceTiming interface:
var myTime = window.performance.now();
Instrumenting your code with marks
The core of User Timing is mark(), which stores a timestamp under a name you choose. The API remembers each named timestamp as a single unit, letting you place a mark at any meaningful point in your application's lifecycle. The specification suggests useful names like mark_fully_loaded, mark_fully_visible, and mark_above_the_fold.
Setting a mark for the moment your application finishes loading is a simple one-liner:
window.performance.mark('mark_fully_loaded');
With marks spread throughout your JavaScript, you end up with a trail of timestamps that can be examined later to understand the sequence and duration of your application's phases.
Computing elapsed time with measure()
Marks alone only give you absolute times. To get useful durations, use measure(), which calculates the time elapsed between two named marks. You can also measure from a known PerformanceTiming event, such as the moment the DOM was complete, up to one of your custom marks.
For instance, to compute the interval from DOM completion to a fully loaded application state:
window.performance.measure('measure_load_from_dom', 'domComplete', 'mark_fully_loaded');
Each call to measure() stores a result independently of the marks themselves. This separation means your application can continue running responsively while you defer analysis until a convenient point.
Clearing marks and measures
Batch runs and repeated tests require a clean slate. The clearMarks() method removes all recorded marks:
window.performance.clearMarks();
If you need to remove only a specific mark, pass its name argument:
window.performance.clearMarks('mark_fully_loaded');
Similarly, clearMeasures() discards measurements in exactly the same fashion. Called with a name, it removes one measure; called without arguments, it clears them all. To remove the measure created in the example above:
window.performance.clearMeasures('measure_load_from_dom');
Extracting timing results
Accumulated marks and measures become useful when you can read them back, which the PerformanceTimeline interface handles. The getEntriesByType() method returns entries in chronological order, so the sequence mirrors what happened at runtime.
Getting every mark in your application:
var items = window.performance.getEntriesByType('mark');
Getting all of your measures:
var items = window.performance.getEntriesByType('measure');
You can also fetch an entry by its exact name, which returns a list with the matching item:
var items = window.performance.getEntriesByName('mark_fully_loaded');
The startTime property holds the timestamp of matching marks retrieved this way.
A practical example: timing XHR requests
To see the API in action, consider instrumenting XMLHttpRequest calls. A typical synchronous sequence of send and callback might look like this:
var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
do_something(e.responseText);
}
myReq.send();
To measure every request, add a counter and a function that sets a mark before the request, then another mark and a measure when the response arrives. A unique measure name for each request keeps the results distinct:
var reqCnt = 0;
var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
window.performance.mark('mark_end_xhr');
reqCnt++;
window.performance.measure('measure_xhr_' + reqCnt, 'mark_start_xhr', 'mark_end_xhr');
do_something(e.responseText);
}
window.performance.mark('mark_start_xhr');
myReq.send();
This approach assumes requests complete in order; parallel requests would need additional bookkeeping to pair responses with their start marks.
Once the application has finished a series of requests, all accumulated timing data can be printed to the console with a small loop:
var items = window.performance.getEntriesByType('measure');
for (var i = 0; i < items.length; ++i) {
var req = items[i];
console.log('XHR ' + req.name + ' took ' + req.duration + 'ms');
}
Where to go from here
The User Timing API is particularly attractive because it works with any part of your application logic, not just network activity. Post-processing the collected data helps you locate hot spots before they become user-facing problems. For environments without native support, a well-regarded polyfill exists that emulates the API and also works with webpagetest.org. Once marks and measures are in place, the path to optimization starts with the clear picture they provide.



