UNPKG

chrome-devtools-frontend

Version:
139 lines (119 loc) 5.18 kB
// Copyright 2024 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as i18n from '../../../core/i18n/i18n.js'; import * as Platform from '../../../core/platform/platform.js'; import * as Handlers from '../handlers/handlers.js'; import * as Helpers from '../helpers/helpers.js'; import * as Types from '../types/types.js'; import { InsightCategory, InsightKeys, type InsightModel, type InsightSetContext, InsightWarning, type PartialInsightModel, } from './types.js'; export const UIStrings = { /** Title of an insight that provides details about if the page's viewport is optimized for mobile viewing. */ title: 'Optimize viewport for mobile', /** * @description Text to tell the user how a viewport meta element can improve performance. \xa0 is a non-breaking space */ description: 'Tap interactions may be [delayed by up to 300\xA0ms](https://developer.chrome.com/docs/performance/insights/viewport) if the viewport is not optimized for mobile.', /** * @description Text for a label describing the portion of an interaction event that was delayed due to a bad mobile viewport. */ mobileTapDelayLabel: 'Mobile tap delay', } as const; const str_ = i18n.i18n.registerUIStrings('models/trace/insights/Viewport.ts', UIStrings); export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export type ViewportInsightModel = InsightModel<typeof UIStrings, { mobileOptimized: boolean | null, viewportEvent?: Types.Events.ParseMetaViewport, longPointerInteractions?: Types.Events.SyntheticInteractionPair[], }>; function finalize(partialModel: PartialInsightModel<ViewportInsightModel>): ViewportInsightModel { return { insightKey: InsightKeys.VIEWPORT, strings: UIStrings, title: i18nString(UIStrings.title), description: i18nString(UIStrings.description), docs: 'https://developer.chrome.com/docs/performance/insights/viewport', category: InsightCategory.INP, state: partialModel.mobileOptimized === false ? 'fail' : 'pass', ...partialModel, }; } export function isViewportInsight(model: InsightModel): model is ViewportInsightModel { return model.insightKey === InsightKeys.VIEWPORT; } export function generateInsight(data: Handlers.Types.HandlerData, context: InsightSetContext): ViewportInsightModel { const viewportEvent = data.UserInteractions.parseMetaViewportEvents.find(event => { if (event.args.data.frame !== context.frameId) { return false; } return Helpers.Timing.eventIsInBounds(event, context.bounds); }); const compositorEvents = data.UserInteractions.beginCommitCompositorFrameEvents.filter(event => { if (event.args.frame !== context.frameId) { return false; } // Commit compositor frame events can be emitted before the viewport tag is parsed. // We shouldn't count these since the browser hasn't had time to make the viewport mobile optimized. if (viewportEvent && event.ts < viewportEvent.ts) { return false; } return Helpers.Timing.eventIsInBounds(event, context.bounds); }); if (!compositorEvents.length) { // Trace doesn't have the data we need. return finalize({ mobileOptimized: null, warnings: [InsightWarning.NO_LAYOUT], }); } // Returns true only if all events are mobile optimized. for (const event of compositorEvents) { if (!event.args.is_mobile_optimized) { // Grab all the pointer events with at least 50ms of input delay. const longPointerInteractions = [...data.UserInteractions.interactionsOverThreshold.values()].filter( interaction => Handlers.ModelHandlers.UserInteractions.categoryOfInteraction(interaction) === 'POINTER' && interaction.inputDelay >= 50_000); // The actual impact varies between 0 and 300. // Using inputDelay as the closest thing we have for measuring this, though inputDelay may be high for other reasons. // b/371566378#comment8 const inputDelay = Math.max(0, ...longPointerInteractions.map(interaction => interaction.inputDelay)) / 1000; const inpMetricSavings = Platform.NumberUtilities.clamp(inputDelay, 0, 300); return finalize({ mobileOptimized: false, viewportEvent, longPointerInteractions, metricSavings: {INP: inpMetricSavings as Types.Timing.Milli}, }); } } return finalize({ mobileOptimized: true, viewportEvent, }); } export function createOverlays(model: ViewportInsightModel): Types.Overlays.Overlay[] { if (!model.longPointerInteractions) { return []; } return model.longPointerInteractions.map(interaction => { const delay = Math.min(interaction.inputDelay, 300 * 1000); const bounds = Helpers.Timing.traceWindowFromMicroSeconds( Types.Timing.Micro(interaction.ts), Types.Timing.Micro(interaction.ts + delay), ); return { type: 'TIMESPAN_BREAKDOWN', entry: interaction, sections: [{bounds, label: i18nString(UIStrings.mobileTapDelayLabel), showDuration: true}], renderLocation: 'ABOVE_EVENT', }; }); }