browsertime icon indicating copy to clipboard operation
browsertime copied to clipboard

Enhance `Navigation timing` metrics. Split absolute and relative metrics, and add new metrics into the table

Open polarnik opened this issue 7 months ago • 4 comments

Feature/improvement

Version

sitespeedio/sitespeed.io:37.5.1-plus1

Current state

Navigation metrics in a report

Image

We have a table with navigation metrics

Navigation Timing

Metric Value
backEndTime 186 ms
domContentLoadedTime 489 ms
domInteractiveTime 489 ms
domainLookupTime 2 ms
frontEndTime 304 ms
pageDownloadTime 0 ms
pageLoadTime 490 ms
redirectionTime 0 ms
serverConnectionTime 76 ms
serverResponseTime 104 ms

Navigation metrics in browsers

https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming

https://mdn.github.io/shared-assets/images/diagrams/api/performance/timestamp-diagram.svg

Image

Improvments

Split absolute and relative metrics

There are absolute metrics from the report. It is a diff between the event and 0 (startTime == 0 always)

Metric Value
backEndTime 186 ms
domContentLoadedTime 489 ms
domInteractiveTime 489 ms
~~domainLookupTime~~ 2 ms
~~frontEndTime~~ 304 ms
~~pageDownloadTime~~ 0 ms
pageLoadTime 490 ms
redirectionTime 0 ms
~~serverConnectionTime~~ 76 ms
~~serverResponseTime~~ 104 ms

The metric pageLoadTime is important here, because it is a full time from start to end. But all other metrics are not important, because they are absolute.

For example the backEndTime is

The time it takes for the network and the server to generate and start sending the HTML. Collected using the Navigation Timing API with the definition: responseStart - navigationStart

It is not about a duration of a backend side, it is about a staring point of a request to the backend side. It is not clear

A table with a title Navigation moments or Navigation starting moments will be better

Metric Value
backEndTime 186 ms
domContentLoadedTime 489 ms
domInteractiveTime 489 ms
pageLoadTime 490 ms
redirectionTime 0 ms

There are relative metrics from the report. It is a diff between the event and some previous event:

Metric Value
~~backEndTime~~ 186 ms
~~domContentLoadedTime~~ 489 ms
~~domInteractiveTime~~ 489 ms
domainLookupTime 2 ms
frontEndTime 304 ms
pageDownloadTime 0 ms
~~pageLoadTime~~ 490 ms
~~redirectionTime~~ 0 ms
serverConnectionTime 76 ms
serverResponseTime 104 ms

Will be better to separate them, and add a clear title like Navigation durations or Navigation steps or Navigation intervals

Metric Value
domainLookupTime 2 ms
frontEndTime 304 ms
pageDownloadTime 0 ms
serverConnectionTime 76 ms
serverResponseTime 104 ms

polarnik avatar May 31 '25 13:05 polarnik

The second suggestion is about adding new steps/intervals about the Navigation timing. I'm talking about blocks and connections between the blocks of the diagram:

  • https://mdn.github.io/shared-assets/images/diagrams/api/performance/timestamp-diagram.svg
Image

I used a custom script defaultPageCompleteCheck.js for getting all navigation steps from the specification:

defaultPageCompleteCheck.js
return (function(waitTime) {

    try {
        console.log(`defaultPageCompleteCheck: waitTime ${waitTime}`);
        var end = window.performance.timing.loadEventEnd;
        console.log(`defaultPageCompleteCheck: end ${end}`);
        var start= window.performance.timing.navigationStart;
        console.log(`defaultPageCompleteCheck: start ${start}`);
        console.log(`defaultPageCompleteCheck: end > 0 : ${end > 0}`);
        console.log(`defaultPageCompleteCheck: end - start + waitTime : ${end - start + waitTime}`);
        console.log(`defaultPageCompleteCheck: performance.now() : ${performance.now()}`);
        console.log(`defaultPageCompleteCheck: return 1 : ${ (end > 0) && (performance.now() > end - start + waitTime) }`);
        let status = (end > 0) && (performance.now() > end - start + waitTime);
        if(status) {
            try {
                const entries = performance.getEntriesByType("navigation");
                const entry = entries[0];

                performance.measure( "0_start_end", {
                    start: 0,
                    duration: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
                });

                performance.measure( "0_startTime_loadEventEnd", {
                    start: 0,
                    duration: entry.loadEventEnd - entry.startTime,
                });


                performance.mark( "0_start_end", {
                    startTime: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
                });

                performance.mark( "0_startTime_loadEventEnd", {
                    startTime: entry.loadEventEnd,
                });


                let index = 1;
                performance.measure(`${index++}_startTime_redirectStart`,
                    {
                        start: entry.startTime,
                        duration: entry.redirectStart - entry.startTime,
                    }
                );

                performance.measure(`${index++}_redirectStart_redirectEnd`,
                    {
                        start: entry.redirectStart,
                        duration: entry.redirectEnd - entry.redirectStart,
                    }
                );

                performance.measure(`${index++}_redirectEnd_workerStart`,
                    {
                        start: entry.redirectEnd,
                        duration: entry.workerStart - entry.redirectEnd,
                    }
                );

                performance.measure(`${index++}_workerStart_fetchStart`,
                    {
                        start: entry.workerStart,
                        duration: entry.fetchStart - entry.workerStart,
                    }
                );

                performance.measure(`${index++}_fetchStart_domainLookupStart`,
                    {
                        start: entry.fetchStart,
                        duration: entry.domainLookupStart - entry.fetchStart,
                    }
                );

                performance.measure(`${index++}_domainLookupStart_domainLookupEnd`,
                    {
                        start: entry.domainLookupStart,
                        duration: entry.domainLookupEnd - entry.domainLookupStart,
                    }
                );

                performance.measure(`${index++}_domainLookupEnd_connectStart`,
                    {
                        start: entry.domainLookupEnd,
                        duration: entry.connectStart - entry.domainLookupEnd,
                    }
                );

                performance.measure(`${index++}_connectStart_secureConnectionStart`,
                    {
                        start: entry.connectStart,
                        duration: entry.secureConnectionStart - entry.connectStart,
                    }
                );

                performance.measure(`${index++}_secureConnectionStart_connectEnd`,
                    {
                        start: entry.secureConnectionStart,
                        duration: entry.connectEnd - entry.secureConnectionStart,
                    }
                );

                performance.measure(`${index++}_connectStart_connectEnd`,
                    {
                        start: entry.connectStart,
                        duration: entry.connectEnd - entry.connectStart,
                    }
                );

                performance.measure(`${index++}_connectEnd_requestStart`,
                    {
                        start: entry.connectEnd,
                        duration: entry.requestStart - entry.connectEnd,
                    }
                );

                performance.measure(`${index++}_requestStart_responseStart`,
                    {
                        start: entry.requestStart,
                        duration: entry.responseStart - entry.requestStart,
                    }
                );

                performance.measure(`${index++}_responseStart_finalResponseHeadersStart`,
                    {
                        start: entry.requestStart,
                        duration: entry.finalResponseHeadersStart - entry.responseStart,
                    }
                );

                performance.measure(`${index++}_finalResponseHeadersStart_responseEnd`,
                    {
                        start: entry.finalResponseHeadersStart,
                        duration: entry.responseEnd - entry.finalResponseHeadersStart,
                    }
                );

                performance.measure(`${index++}_finalResponseHeadersStart_responseEnd`,
                    {
                        start: entry.finalResponseHeadersStart,
                        duration: entry.responseEnd - entry.finalResponseHeadersStart,
                    }
                );

                performance.measure(`${index++}_responseStart_responseEnd`,
                    {
                        start: entry.responseStart,
                        duration: entry.responseEnd - entry.responseStart,
                    }
                );

                performance.measure(`${index++}_unloadEventStart_unloadEventEnd`,
                    {
                        start: entry.unloadEventStart,
                        duration: entry.unloadEventEnd - entry.unloadEventStart,
                    }
                );

                performance.measure(`${index++}_responseEnd_domInteractive`,
                    {
                        start: entry.responseEnd,
                        duration: entry.domInteractive - entry.responseEnd,
                    }
                );

                performance.measure(`${index++}_domInteractive_domContentLoadedEventStart`,
                    {
                        start: entry.domInteractive,
                        duration: entry.domContentLoadedEventStart - entry.domInteractive,
                    }
                );

                performance.measure(`${index++}_domContentLoadedEventStart_domContentLoadedEventEnd`,
                    {
                        start: entry.domContentLoadedEventStart,
                        duration: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
                    }
                );

                performance.measure(`${index++}_domContentLoadedEventEnd_domComplete`,
                    {
                        start: entry.domContentLoadedEventEnd,
                        duration: entry.domComplete - entry.domContentLoadedEventEnd,
                    }
                );

                performance.measure(`${index++}_domComplete_loadEventStart`,
                    {
                        start: entry.domComplete,
                        duration: entry.loadEventStart - entry.domComplete,
                    }
                );

                performance.measure(`${index++}_loadEventStart_loadEventEnd`,
                    {
                        start: entry.loadEventStart,
                        duration: entry.loadEventEnd - entry.loadEventStart,
                    }
                );

                const measures_my = [];
                const marks_my = [];

                if (window.performance && window.performance.getEntriesByType) {
                    const myMarks = Array.prototype.slice.call(
                        window.performance.getEntriesByType('mark')
                    );

                    for (const mark of myMarks) {
                        if (mark.detail && mark.detail.devtools) {
                            continue;
                        } else {
                            marks_my.push({
                                name: mark.name,
                                startTime: mark.startTime
                            });
                        }
                    }

                    const myMeasures = Array.prototype.slice.call(
                        window.performance.getEntriesByType('measure')
                    );

                    for (const measure of myMeasures) {
                        if (measure.detail && measure.detail.devtools) {
                            continue;
                        } else {
                            measures_my.push({
                                name: measure.name,
                                duration: measure.duration,
                                startTime: measure.startTime
                            });
                        }
                    }
                }

                console.log(`defaultPageCompleteCheck: measures_my: \n${JSON.stringify(measures_my)}`)
                console.log(`defaultPageCompleteCheck: marks_my: \n${JSON.stringify(marks_my)}`)
            } catch (e) {
                console.log(`defaultPageCompleteCheck: exception: \n${JSON.stringify(e)}`)
            }
            return true;
        } else {
            return false;
        }
    } 
    catch(e) {
        console.log(`defaultPageCompleteCheck: e : ${JSON.stringify(e)}`);
        console.log(`defaultPageCompleteCheck: return 2 : true`);
        return true;
    }
})(arguments[arguments.length - 1]);

There are my outcomes:

Image Image

The longest steps/intervals for Chrome pages with an empty cache are:

  1. responseEnd ... domInteractive or 18_responseEnd_domInteractive up to 70%
  2. requestStart ... responseStart or 12_requestStart_responseStart up to 10-20%
  3. connectStart ... secureConnectionStart or 8_connectStart_secureConnectionStart 10-20%
  4. secureConnectionStart ... connectEnd or 9_secureConnectionStart_connectEnd 10-20%
  5. domContentLoadedEventEnd ... domComplete or 21_domContentLoadedEventEnd_domComplete it is equal to 0 for 99% of all tests, but it is 50% for 1% of tests

I think we can add those TOP-5 navigation interval as well, or we can add all intervals into the report

polarnik avatar May 31 '25 14:05 polarnik

By the way, what do you think about the intervals:

  • responseEnd ... domInteractive or 18_responseEnd_domInteractive up to 70%
  • domContentLoadedEventEnd ... domComplete or 21_domContentLoadedEventEnd_domComplete it is equal to 0 for 99% of all tests, but it is 50% for 1% of tests

@soulgalore

?

Image

There are two invisible intervals from the specification, but there are the longest intervals for the Chromium 136.0.7103.25 metrics. How is it possible?

Is it an issue of the Chromium 136.0.7103.25 ?

Because the interval responseEnd ... domInteractive has significant improvements for the repeat-tests (the tests with a hot cache):

Image Image

The interval domContentLoadedEventStart ... domContentLoadedEventEnd or 20_domContentLoadedEventStart_domContentLoadedEventEnd should have improvements for the repeat tests, but this interval is equal to 0 for all tests.

I think the Chromium 136.0.7103.25 has an issue, it collects the DOM-Tree-duration as responseEnd ... domInteractive instead of domContentLoadedEventStart ... domContentLoadedEventEnd

polarnik avatar May 31 '25 14:05 polarnik

My motivation for adding intervals / steps / blocks

We can visualize specific blocks via the Bar Chart diagram in Grafana, for the example the TCP block:

Image

polarnik avatar May 31 '25 17:05 polarnik

Sorry I don't follow along, what do you want to do? :) Can you explain what you want to do and what you will gain with it? We collect the Navigation Timing raw data in https://github.com/sitespeedio/browsertime/blob/main/browserscripts/timings/navigationTiming.js and it's sent to Graphite by default, so I think you can create the bar charts using those if they are useful for you.

The metrics in https://github.com/sitespeedio/browsertime/blob/main/browserscripts/timings/pageTimings.js is how GA reported metrics long time ago. I kept those in the HTML report and then use the raw metrics only in the time series database so you can create graphs for it. Maybe we could include those raw values too if we can make it clean in the HTML.

There are two invisible intervals from the specification

I would check that with the Chrome team if its not in the specification.

soulgalore avatar Jun 01 '25 05:06 soulgalore