lighthouse
Version:
Automated auditing, performance metrics, and best practices for the web.
88 lines (75 loc) • 3.4 kB
JavaScript
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {calculateDocFirstByteTs} from '@paulirish/trace_engine/models/trace/insights/Common.js';
import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {MainResource} from '../main-resource.js';
import {NetworkAnalysis} from '../network-analysis.js';
import {NavigationInsights} from '../navigation-insights.js';
import {TraceEngineResult} from '../trace-engine-result.js';
class TimeToFirstByte extends NavigationMetric {
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeSimulatedMetric(data, context) {
const mainResource = await MainResource.request(data, context);
const networkAnalysis = await NetworkAnalysis.request(data.devtoolsLog, context);
const observedTTFB = (await this.computeObservedMetric(data, context)).timing;
const observedResponseTime =
networkAnalysis.serverResponseTimeByOrigin.get(mainResource.parsedURL.securityOrigin);
if (observedResponseTime === undefined) throw new Error('No response time for origin');
// Estimate when the connection is not warm.
// TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
let roundTrips = 2;
if (!mainResource.protocol.startsWith('h3')) roundTrips += 1; // TCP
if (mainResource.parsedURL.scheme === 'https') roundTrips += 1;
const estimatedTTFB = data.settings.throttling.rttMs * roundTrips + observedResponseTime;
const timing = Math.max(observedTTFB, estimatedTTFB);
return {timing};
}
/**
* @param {LH.Artifacts.NavigationMetricComputationData} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data, context) {
const {trace, settings, SourceMaps, HostDPR} = data;
const traceEngineResult =
await TraceEngineResult.request({trace, settings, SourceMaps, HostDPR}, context);
const navInsights =
await NavigationInsights.request({trace, settings, SourceMaps, HostDPR}, context);
const lcpBreakdown = navInsights.model.LCPBreakdown;
// Defer to LCP breakdown, but if there's no LCP fallback to manual calculation.
if (!(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
return {
timing: lcpBreakdown.subparts.ttfb.range / 1000,
timestamp: lcpBreakdown.subparts.ttfb.max,
};
} else if (navInsights.navigation?.args.data?.navigationId) {
const request = traceEngineResult.data.NetworkRequests.byId.get(
navInsights.navigation.args.data.navigationId);
if (!request) {
throw new Error();
}
const timestamp = calculateDocFirstByteTs(request);
if (timestamp === null) {
throw new Error('cannot calculate ttfb');
}
return {
timing: (timestamp - navInsights.navigation.ts) / 1000,
timestamp,
};
}
throw new Error('cannot determine ttfb');
}
}
const TimeToFirstByteComputed = makeComputedArtifact(
TimeToFirstByte,
['devtoolsLog', 'gatherContext', 'settings', 'simulator', 'trace', 'URL', 'SourceMaps', 'HostDPR']
);
export {TimeToFirstByteComputed as TimeToFirstByte};