| // 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 type * as Common from '../../../core/common/common.js'; |
| import * as i18n from '../../../core/i18n/i18n.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, |
| type PartialInsightModel, |
| } from './types.js'; |
| |
| export const UIStrings = { |
| /** |
| * @description Title of an insight that recommends reducing the size of the DOM tree as a means to improve page responsiveness. "DOM" is an acronym and should not be translated. |
| */ |
| title: 'Optimize DOM size', |
| /** |
| * @description Description of an insight that recommends reducing the size of the DOM tree as a means to improve page responsiveness. "DOM" is an acronym and should not be translated. "layout reflows" are when the browser will recompute the layout of content on the page. |
| */ |
| description: |
| 'A large DOM can increase the duration of style calculations and layout reflows, impacting page responsiveness. A large DOM will also increase memory usage. [Learn how to avoid an excessive DOM size](https://developer.chrome.com/docs/performance/insights/dom-size).', |
| /** |
| * @description Header for a column containing the names of statistics as opposed to the actual statistic values. |
| */ |
| statistic: 'Statistic', |
| /** |
| * @description Header for a column containing the value of a statistic. |
| */ |
| value: 'Value', |
| /** |
| * @description Header for a column containing the page element related to a statistic. |
| */ |
| element: 'Element', |
| /** |
| * @description Label for a value representing the total number of elements on the page. |
| */ |
| totalElements: 'Total elements', |
| /** |
| * @description Label for a value representing the maximum depth of the Document Object Model (DOM). "DOM" is a acronym and should not be translated. |
| */ |
| maxDOMDepth: 'DOM depth', |
| /** |
| * @description Label for a value representing the maximum number of child elements of any parent element on the page. |
| */ |
| maxChildren: 'Most children', |
| /** |
| * @description Text for a section. |
| */ |
| topUpdatesDescription: |
| 'These are the largest layout and style recalculation events. Their performance impact may be reduced by making the DOM simpler.', |
| /** |
| * @description Label used for a time duration. |
| */ |
| duration: 'Duration', |
| /** |
| * @description Message displayed in a table detailing how big a layout (rendering) is. |
| * @example {134} PH1 |
| */ |
| largeLayout: 'Layout ({PH1} objects)', |
| /** |
| * @description Message displayed in a table detailing how big a style recalculation (rendering) is. |
| * @example {134} PH1 |
| */ |
| largeStyleRecalc: 'Style recalculation ({PH1} elements)', |
| } as const; |
| |
| const str_ = i18n.i18n.registerUIStrings('models/trace/insights/DOMSize.ts', UIStrings); |
| export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| const DOM_SIZE_DURATION_THRESHOLD = Helpers.Timing.milliToMicro(Types.Timing.Milli(40)); |
| |
| // These thresholds were selected to maximize the number of long (>40ms) events above |
| // the threshold while maximizing the number of short (<40ms) events below the threshold. |
| // See go/rpp-dom-size-thresholds for the analysis that produced these thresholds. |
| const LAYOUT_OBJECTS_THRESHOLD = 100; |
| const STYLE_RECALC_ELEMENTS_THRESHOLD = 300; |
| |
| export type DOMSizeInsightModel = InsightModel<typeof UIStrings, { |
| largeLayoutUpdates: Types.Events.Layout[], |
| largeStyleRecalcs: Types.Events.RecalcStyle[], |
| largeUpdates: Array< |
| {label: Common.UIString.LocalizedString, duration: Types.Timing.Milli, size: number, event: Types.Events.Event}>, |
| maxDOMStats?: Types.Events.DOMStats, |
| }>; |
| |
| function finalize(partialModel: PartialInsightModel<DOMSizeInsightModel>): DOMSizeInsightModel { |
| const relatedEvents = [...partialModel.largeLayoutUpdates, ...partialModel.largeStyleRecalcs]; |
| return { |
| insightKey: InsightKeys.DOM_SIZE, |
| strings: UIStrings, |
| title: i18nString(UIStrings.title), |
| description: i18nString(UIStrings.description), |
| docs: 'https://developer.chrome.com/docs/performance/insights/dom-size', |
| category: InsightCategory.INP, |
| state: relatedEvents.length > 0 ? 'informative' : 'pass', |
| ...partialModel, |
| relatedEvents, |
| }; |
| } |
| |
| export function isDomSizeInsight(model: InsightModel): model is DOMSizeInsightModel { |
| return model.insightKey === InsightKeys.DOM_SIZE; |
| } |
| |
| export function generateInsight(data: Handlers.Types.HandlerData, context: InsightSetContext): DOMSizeInsightModel { |
| const isWithinContext = (event: Types.Events.Event): boolean => Helpers.Timing.eventIsInBounds(event, context.bounds); |
| |
| const mainTid = context.navigation?.tid; |
| |
| const largeLayoutUpdates: Types.Events.Layout[] = []; |
| const largeStyleRecalcs: Types.Events.RecalcStyle[] = []; |
| |
| const threads = Handlers.Threads.threadsInRenderer(data.Renderer, data.AuctionWorklets); |
| for (const thread of threads) { |
| if (thread.type !== Handlers.Threads.ThreadType.MAIN_THREAD) { |
| continue; |
| } |
| |
| if (mainTid === undefined) { |
| // We won't have a specific thread ID to reference if the context does not have a navigation. |
| // In this case, we'll just filter out any OOPIFs threads. |
| if (!thread.processIsOnMainFrame) { |
| continue; |
| } |
| } else if (thread.tid !== mainTid) { |
| continue; |
| } |
| |
| const rendererThread = data.Renderer.processes.get(thread.pid)?.threads.get(thread.tid); |
| if (!rendererThread) { |
| continue; |
| } |
| |
| const {entries, layoutEvents, recalcStyleEvents} = rendererThread; |
| if (!entries.length) { |
| continue; |
| } |
| |
| const first = entries[0]; |
| const last = entries[entries.length - 1]; |
| const timeRange = |
| Helpers.Timing.traceWindowFromMicroSeconds(first.ts, Types.Timing.Micro(last.ts + (last.dur ?? 0))); |
| if (!Helpers.Timing.boundsIncludeTimeRange({timeRange, bounds: context.bounds})) { |
| continue; |
| } |
| |
| for (const event of layoutEvents) { |
| if (event.dur < DOM_SIZE_DURATION_THRESHOLD || !isWithinContext(event)) { |
| continue; |
| } |
| |
| const {dirtyObjects} = event.args.beginData; |
| if (dirtyObjects > LAYOUT_OBJECTS_THRESHOLD) { |
| largeLayoutUpdates.push(event); |
| } |
| } |
| |
| for (const event of recalcStyleEvents) { |
| if (event.dur < DOM_SIZE_DURATION_THRESHOLD || !isWithinContext(event)) { |
| continue; |
| } |
| |
| const {elementCount} = event.args; |
| if (elementCount > STYLE_RECALC_ELEMENTS_THRESHOLD) { |
| largeStyleRecalcs.push(event); |
| } |
| } |
| } |
| |
| const largeUpdates: DOMSizeInsightModel['largeUpdates'] = [ |
| ...largeLayoutUpdates.map(event => { |
| const duration = (event.dur / 1000) as Types.Timing.Milli; |
| const size = event.args.beginData.dirtyObjects; |
| const label = i18nString(UIStrings.largeLayout, {PH1: size}); |
| return {label, duration, size, event}; |
| }), |
| ...largeStyleRecalcs.map(event => { |
| const duration = (event.dur / 1000) as Types.Timing.Milli; |
| const size = event.args.elementCount; |
| const label = i18nString(UIStrings.largeStyleRecalc, {PH1: size}); |
| return {label, duration, size, event}; |
| }), |
| ].sort((a, b) => b.duration - a.duration).slice(0, 5); |
| |
| const domStatsEvents = data.DOMStats.domStatsByFrameId.get(context.frameId)?.filter(isWithinContext) ?? []; |
| let maxDOMStats: Types.Events.DOMStats|undefined; |
| for (const domStats of domStatsEvents) { |
| // While recording a cross-origin navigation, there can be overlapping dom stats from before & after |
| // the navigation which share a frameId. In this case we should also ensure the pid matches up with |
| // the navigation we care about (i.e. from after the navigation event). |
| const navigationPid = context.navigation?.pid; |
| if (navigationPid && domStats.pid !== navigationPid) { |
| continue; |
| } |
| |
| if (!maxDOMStats || domStats.args.data.totalElements > maxDOMStats.args.data.totalElements) { |
| maxDOMStats = domStats; |
| } |
| } |
| |
| return finalize({ |
| largeLayoutUpdates, |
| largeStyleRecalcs, |
| largeUpdates, |
| maxDOMStats, |
| }); |
| } |
| |
| export function createOverlays(model: DOMSizeInsightModel): Types.Overlays.Overlay[] { |
| const entries = [...model.largeStyleRecalcs, ...model.largeLayoutUpdates]; |
| return entries.map(entry => ({ |
| type: 'ENTRY_OUTLINE', |
| entry, |
| outlineReason: 'ERROR', |
| })); |
| } |