From 063f934d3d4f67392ab53dc5bf779a51c5e9110f Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Sun, 25 Oct 2026 21:14:37 +0100 Subject: [PATCH] Measure Total Blocking Time per soft navigation On a soft navigation the document stays the same, so the long task buffer still holds the work of every earlier route. Total Blocking Time grew with every step of a user journey: four routes doing the same 200 ms of blocking work reported 451, 651, 851 and 1051 ms. Later pages then failed budgets because of earlier pages' work. FCP and LCP were already measured from the soft navigation start but the long tasks were not, so nearly every task counted. Long tasks are now limited to the measured soft navigation, the same way CLS already is, and use the same start point as the paint metrics. Fixes sitespeedio/sitespeed.io#4892 Assisted-by: Claude Opus 5.5 noreply@anthropic.com --- lib/chrome/longTaskMetrics.js | 26 +++++++++++++-- lib/chrome/webdriver/chromium.js | 1 + test/unittests/longTaskMetricsTest.js | 48 +++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 test/unittests/longTaskMetricsTest.js diff --git a/lib/chrome/longTaskMetrics.js b/lib/chrome/longTaskMetrics.js index 74ec0970c5..47735bf4a9 100644 --- a/lib/chrome/longTaskMetrics.js +++ b/lib/chrome/longTaskMetrics.js @@ -37,7 +37,29 @@ export function longTaskMetrics(result, options) { let longTasksAfterLoadEventEnd = 0; let lastLongTask = 0; - for (let longTask of result.browserScripts.pageinfo.longTask) { + let longTasks = result.browserScripts.pageinfo.longTask; + // A soft navigation keeps the document, so the buffer also has the long + // tasks of earlier routes. The paint metrics are relative to the soft + // navigation start, so make the long tasks relative to it too. + const softNavigation = getProperty( + result, + 'browserScripts.timings.softNavigations', + [] + ).at(-1); + if (softNavigation) { + longTasks = longTasks + .filter(longTask => + longTask.navigationId === undefined + ? longTask.startTime >= softNavigation.startTime + : longTask.navigationId === softNavigation.navigationId + ) + .map(longTask => ({ + ...longTask, + startTime: longTask.startTime - softNavigation.startTime + })); + } + + for (let longTask of longTasks) { totalDuration += longTask.duration; if (longTask.startTime > lastLongTask) { lastLongTask = longTask.startTime; @@ -73,7 +95,7 @@ export function longTaskMetrics(result, options) { const cpu = { longTasks: { - tasks: result.browserScripts.pageinfo.longTask.length, + tasks: longTasks.length, totalDuration, totalBlockingTime: Number(totalBlockingTime.toFixed(0)), maxPotentialFid: Number(maxPotentialFid.toFixed(0)), diff --git a/lib/chrome/webdriver/chromium.js b/lib/chrome/webdriver/chromium.js index d9b9458315..b8fbcfab15 100644 --- a/lib/chrome/webdriver/chromium.js +++ b/lib/chrome/webdriver/chromium.js @@ -245,6 +245,7 @@ export class Chromium { e.duration = event.duration; e.name = event.name; e.startTime = event.startTime; + e.navigationId = event.navigationId; e.attribution = []; for (let at of event.attribution) { const a = {}; diff --git a/test/unittests/longTaskMetricsTest.js b/test/unittests/longTaskMetricsTest.js new file mode 100644 index 0000000000..b53b3ed3da --- /dev/null +++ b/test/unittests/longTaskMetricsTest.js @@ -0,0 +1,48 @@ +import test from 'ava'; + +import { longTaskMetrics } from '../../lib/chrome/longTaskMetrics.js'; + +function result(longTask, softNavigations) { + return { + browserScripts: { + timings: { + paintTiming: { 'first-contentful-paint': 40 }, + softNavigations + }, + pageinfo: { longTask } + } + }; +} + +const tasks = [ + { startTime: 1000, duration: 250, navigationId: 'hard' }, + { startTime: 5000, duration: 250, navigationId: 'route-one' }, + { startTime: 9000, duration: 250, navigationId: 'route-two' } +]; + +test('Total Blocking Time uses every long task after FCP for a page load', t => { + const cpu = longTaskMetrics(result([{ startTime: 1000, duration: 250 }]), {}); + t.is(cpu.longTasks.totalBlockingTime, 200); +}); + +test('Total Blocking Time only uses the long tasks of the soft navigation', t => { + const cpu = longTaskMetrics( + result(tasks, [{ navigationId: 'route-two', startTime: 8950 }]), + {} + ); + t.is(cpu.longTasks.tasks, 1); + t.is(cpu.longTasks.totalBlockingTime, 200); + t.is(cpu.longTasks.lastLongTask, 50); +}); + +test('Soft navigation long tasks without navigationId use the start time', t => { + const cpu = longTaskMetrics( + result( + tasks.map(({ startTime, duration }) => ({ startTime, duration })), + [{ navigationId: 'route-two', startTime: 8950 }] + ), + {} + ); + t.is(cpu.longTasks.tasks, 1); + t.is(cpu.longTasks.totalBlockingTime, 200); +});