Search code examples
javascriptfirefox-developer-toolspage-load-timeload-time

Which are the right metrics to accurately measure page load time?


I'm using the JavaScript Performance API and I'm trying to figure out the right combination of metrics to log my page load time on the console, whenever any individual page is requested and loads.

TLDR: I want to use JavaScript's Performance API to get a number close to the number reported by load on the Network tab of Firefox Developer Tools (or any browser developer tools).

See the number on the right hand side in the image immediately below:

Firefox Developer Tools Performance Metrics

I wouldn't have an issue but for the fact that no combination that I've yet tried comes consistently close to the number reported by load on the Network tab of Firefox Developer Tools - sometimes the final number I get is up to several hundredths of a second under, sometimes the same amount over.

It may be that I'm already achieving numbers as close as I can get, but I want to be sure that I am and not accidentally referring to inappropriate metrics.

Here are the metrics I'm using (from the PerformanceNavigationTiming interface of the Performance API):

  • domainLookupEnd
  • connectEnd - I was using this before, but not currently
  • responseEnd
  • loadEventStart

And here's what I have at present:

window.addEventListener('load', () => {
  
  let domainLookupEnd = performance.getEntriesByType('navigation')[0].domainLookupEnd;
  let connectEnd = performance.getEntriesByType('navigation')[0].connectEnd;
  let responseEnd = performance.getEntriesByType('navigation')[0].responseEnd;
  let loadEventStart = performance.getEntriesByType('navigation')[0].loadEventStart;
 
  console.log(`
domainLookupEnd: ${domainLookupEnd}
connectEnd: ${connectEnd}
responseEnd: ${responseEnd}
loadEventStart: ${loadEventStart}

Page loaded in: (${responseEnd} - ${domainLookupEnd})
Page built in: (${loadEventStart} - ${responseEnd})
Page loaded and built in: (${loadEventStart} - ${domainLookupEnd})

// ^^^ All this is just temporary helper info to ensure that the three lines below are correct 

Page loaded in: ${((responseEnd - domainLookupEnd) / 1000)} seconds.
Page built in: ${((loadEventStart - responseEnd) / 1000)} seconds.
Page loaded and built in: ${((loadEventStart - domainLookupEnd) / 1000)} seconds.
  `);
});

Fairly consistently I find that the metrics reported in the javascript console are between one thousandth (0.001) and (as much as) four hundredths (0.04) of a second less than the load time reported by the Network tab.

Is this the best I can hope for, or am I doing something wrong / choosing the wrong metrics?


Solution

  • Your times are likely faster because you are measuring less

    Let's start with MDN's very excellent illustration from Measuring performance:

    The various handlers that the navigation timing API can handle including Navigation timing API metrics Prompt for unload redirect unload App cache DNS TCP Request Response Processing onLoad navigationStart redirectStart redirectEnd fetchStart domainLookupEnd domainLookupStart connectStart (secureConnectionStart) connectEnd requestStart responseStart responseEnd unloadStart unloadEnd domLoading domInteractive domContentLoaded domComplete loadEventStart loadEventEnd

    🚩 The earliest segment moment you include is domainLookupEnd, which means only the stuff after the green DNS lookup in the graph.

    MDN and Mozilla include more

    🚩 All of the MDN and Mozilla docs below consider navigationStart the beginning of the page load time measurement, and go at least to loadEventStart, if not to loadEventEnd.

    MDN's Page load time doc says:

    Page load time is the time it takes for a page to load, measured from navigation start to the start of the load event.

    let time = performance.timing;
    
    let pageloadtime = time.loadEventStart - time.navigationStart;
    

    The MDN's Navigation Timing API doc has a slightly different calculation of page load time:

    Calculate the total page load time

    To compute the total amount of time it took to load the page, you can use the following code:

    const perfData = window.performance.timing;
    const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
    

    This subtracts the time at which navigation began (navigationStart) from the time at which the load event handler returns (loadEventEnd). This gives you the perceived page load time.

    Mozilla's Improving Firefox Page Load (2020) blog post:

    We define page load as the time between clicking a link, or pressing enter in the URL bar, to the time that a page is displayed in the browser and ready to be used.

    Mozilla's Comparing Browser Page Load Time: An Introduction to Methodology (2017) seems to go with loadEventEnd:

    loadEventEnd represents the moment when the load event for the requested page is completed, i.e all static content of the page is fully loaded.

    It is a valid concern that loadEventEnd may not be the best indicator for what users experience on screen when loading a page. However, both loadEventEnd as well as average session load time were recently found to be good predictors for user bounce rate.

    Your may also be measuring cache hits instead of fresh web requests

    Firefox Developer Tools shows both separately, so you should make sure you are using the set of numbers that matches how your code's requests are handled.

    How to include the onLoad segment in your measurement

    If Firefox is including the entire onLoad segment in its measurement, it's including the execution time of all onLoad or load event scripts. This currently includes the execution time of your own measurement script!

    That might explain why sometimes it differs by so little ("one thousandth") and sometimes by more ("four hundredths"): on pages where your measurement script is the only onLoad script, it differs by just the time it takes to execute that script, and on pages that have more scripts it takes more time and the difference in measurement is larger.

    There is no standard event to trigger execution after all onLoad scripts are done. So instead you'll have to schedule it later from within onLoad:

    // 2 sec delay to cover page with heavy onLoad scripts, may need
    // to lengthen, or maybe can shorten if all your pages are fast.
    setTimeout(() => {
        //your measurement code or call to measurement function here
    }, 2000)
    
    

    other useful resources

    Measuring Web Performance in 2022: The Definitive Guide