Open10
lighthouse を読む Day 2
今日は動的解析する
$ yarn install
$ yarn update:sample-json
# install path
$ CHROME_PATH=/snap/bin/chromium node cli/index.js
// cli/index.js
import {begin} from './bin.js';
await begin();
/// cli/bin.js
/**
* @return {Promise<LH.RunnerResult|void>}
*/
async function begin() {
// ....
return runLighthouse(urlUnderTest, cliFlags, config);
}
ここまで config の読み込み
/// cli/run.js
import lighthouse from '../core/index.js';
/**
* @param {string} url
* @param {LH.CliFlags} flags
* @param {LH.Config|undefined} config
* @return {Promise<LH.RunnerResult|undefined>}
*/
async function runLighthouse(url, flags, config) {
/// ...
const shouldGather = flags.gatherMode || flags.gatherMode === flags.auditMode;
const shouldUseLocalChrome = UrlUtils.isLikeLocalhost(flags.hostname);
if (shouldGather && shouldUseLocalChrome) {
launchedChrome = await getDebuggableChrome(flags);
flags.port = launchedChrome.port;
}
///...
const runnerResult = await lighthouse(url, flags, config);
// If in gatherMode only, there will be no runnerResult.
if (runnerResult) {
await saveResults(runnerResult, flags);
}
launchedChrome?.kill();
process.removeListener('unhandledRejection', handleTheUnhandled);
// Runtime errors indicate something was *very* wrong with the page result.
// We don't want the user to have to parse the report to figure it out, so we'll still exit
// with an error code after we saved the results.
if (runnerResult?.lhr.runtimeError) {
const {runtimeError} = runnerResult.lhr;
return printErrorAndExit({
name: 'LighthouseError',
friendlyMessage: runtimeError.message,
code: runtimeError.code,
message: runtimeError.message,
});
}
return runnerResult;
} catch (err) {
launchedChrome?.kill();
return printErrorAndExit(err);
}
}
デバッグ用Chromeを起動して、debug port を core の lighthouse に渡す
console を挟んで確認
async function _setup({ driver, resolvedConfig, requestor }) {
await driver.connect();
/// about:blank を起動
// We can't trigger the navigation through user interaction if we reset the page before starting.
if (
typeof requestor === "string" &&
!resolvedConfig.settings.skipAboutBlank
) {
// Disable network monitor on the blank page to prevent it from picking up network requests and
// frame navigated events before the run starts.
await driver._networkMonitor?.disable();
await gotoURL(driver, resolvedConfig.settings.blankPage, {
waitUntil: ["navigated"],
});
await driver._networkMonitor?.enable();
}
/// Chromeに向けてベンチマークをする
const baseArtifacts = await getBaseArtifacts(resolvedConfig, driver, {
gatherMode: "navigation",
});
const { warnings } = await prepare.prepareTargetForNavigationMode(
driver,
resolvedConfig.settings,
requestor
);
baseArtifacts.LighthouseRunWarnings.push(...warnings);
return { baseArtifacts };
}
_navigation でどういう artifactsDefinistions が初期化されているか
/**
* @param {NavigationContext} navigationContext
* @return {ReturnType<typeof _computeNavigationResult>}
*/
async function _navigation(navigationContext) {
if (!navigationContext.resolvedConfig.artifacts) {
throw new Error("No artifacts were defined on the config");
}
const artifactState = getEmptyArtifactState();
const phaseState = {
url: await navigationContext.driver.url(),
gatherMode: /** @type {const} */ ("navigation"),
driver: navigationContext.driver,
page: navigationContext.page,
computedCache: navigationContext.computedCache,
artifactDefinitions: navigationContext.resolvedConfig.artifacts,
artifactState,
baseArtifacts: navigationContext.baseArtifacts,
settings: navigationContext.resolvedConfig.settings,
};
const disableAsyncStacks = await prepare.enableAsyncStacks(
navigationContext.driver.defaultSession
);
console.log("----_navigation----");
console.log(phaseState);
throw "stop";
artifactDefinitions: [
{ id: 'DevtoolsLog', gatherer: [Object], dependencies: undefined },
{ id: 'Trace', gatherer: [Object], dependencies: undefined },
{ id: 'RootCauses', gatherer: [Object], dependencies: [Object] },
{
id: 'Accessibility',
gatherer: [Object],
dependencies: undefined
},
{
id: 'AnchorElements',
gatherer: [Object],
dependencies: undefined
},
{
id: 'ConsoleMessages',
gatherer: [Object],
dependencies: undefined
},
{ id: 'CSSUsage', gatherer: [Object], dependencies: undefined },
{ id: 'Doctype', gatherer: [Object], dependencies: undefined },
{ id: 'DOMStats', gatherer: [Object], dependencies: undefined },
{ id: 'FontSize', gatherer: [Object], dependencies: undefined },
{ id: 'Inputs', gatherer: [Object], dependencies: undefined },
{
id: 'ImageElements',
gatherer: [Object],
dependencies: undefined
},
{
id: 'InspectorIssues',
gatherer: [Object],
dependencies: [Object]
},
{ id: 'JsUsage', gatherer: [Object], dependencies: undefined },
{ id: 'LinkElements', gatherer: [Object], dependencies: [Object] },
{
id: 'MainDocumentContent',
gatherer: [Object],
dependencies: [Object]
},
{ id: 'MetaElements', gatherer: [Object], dependencies: undefined },
{
id: 'NetworkUserAgent',
gatherer: [Object],
dependencies: [Object]
},
{
id: 'OptimizedImages',
gatherer: [Object],
dependencies: [Object]
},
{
id: 'ResponseCompression',
gatherer: [Object],
dependencies: [Object]
},
{ id: 'RobotsTxt', gatherer: [Object], dependencies: undefined },
{ id: 'Scripts', gatherer: [Object], dependencies: undefined },
{ id: 'SourceMaps', gatherer: [Object], dependencies: [Object] },
{ id: 'Stacks', gatherer: [Object], dependencies: undefined },
{ id: 'Stylesheets', gatherer: [Object], dependencies: undefined },
{ id: 'TraceElements', gatherer: [Object], dependencies: [Object] },
{
id: 'ViewportDimensions',
gatherer: [Object],
dependencies: undefined
},
{ id: 'devtoolsLogs', gatherer: [Object], dependencies: [Object] },
{ id: 'traces', gatherer: [Object], dependencies: [Object] },
{
id: 'FullPageScreenshot',
gatherer: [Object],
dependencies: undefined
},
{
id: 'BFCacheFailures',
gatherer: [Object],
dependencies: [Object]
}
],
artifactState: {
startInstrumentation: {},
startSensitiveInstrumentation: {},
stopSensitiveInstrumentation: {},
stopInstrumentation: {},
getArtifact: {}
},
gatherer
各gathererは段階ごとにデータを集計する。
const phase: "getArtifact" | "startInstrumentation" | "startSensitiveInstrumentation" | "stopSensitiveInstrumentation" | "stopInstrumentation"
Gatherer の例
core/gather/gatherers/meta-elements.js
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import BaseGatherer from '../base-gatherer.js';
import {pageFunctions} from '../../lib/page-functions.js';
/* globals getElementsInDocument getNodeDetails */
/* c8 ignore start */
function collectMetaElements() {
const functions = /** @type {typeof pageFunctions} */({
// @ts-expect-error - getElementsInDocument put into scope via stringification
getElementsInDocument,
// @ts-expect-error - getNodeDetails put into scope via stringification
getNodeDetails,
});
const metas = functions.getElementsInDocument('head meta');
return metas.map(meta => {
/** @param {string} name */
const getAttribute = name => {
const attr = meta.attributes.getNamedItem(name);
if (!attr) return;
return attr.value;
};
return {
name: meta.name.toLowerCase(),
content: meta.content,
property: getAttribute('property'),
httpEquiv: meta.httpEquiv ? meta.httpEquiv.toLowerCase() : undefined,
charset: getAttribute('charset'),
node: functions.getNodeDetails(meta),
};
});
}
/* c8 ignore stop */
class MetaElements extends BaseGatherer {
/** @type {LH.Gatherer.GathererMeta} */
meta = {
supportedModes: ['snapshot', 'navigation'],
};
/**
* @param {LH.Gatherer.Context} passContext
* @return {Promise<LH.Artifacts['MetaElements']>}
*/
getArtifact(passContext) {
const driver = passContext.driver;
// We'll use evaluateAsync because the `node.getAttribute` method doesn't actually normalize
// the values like access from JavaScript does.
return driver.executionContext.evaluate(collectMetaElements, {
args: [],
useIsolation: true,
deps: [
pageFunctions.getElementsInDocument,
pageFunctions.getNodeDetails,
],
});
}
}
export default MetaElements;
metaに定義されたdependencies ごとにソートして実行
async function collectPhaseArtifacts(options) {
const {
driver,
page,
artifactDefinitions,
artifactState,
baseArtifacts,
phase,
gatherMode,
computedCache,
settings,
} = options;
const priorPhase = phaseToPriorPhase[phase];
const priorPhaseArtifacts = (priorPhase && artifactState[priorPhase]) || {};
const isFinalPhase = phase === 'getArtifact';
for (const artifactDefn of artifactDefinitions) {
log.verbose(`artifacts:${phase}`, artifactDefn.id);
const gatherer = artifactDefn.gatherer.instance;
const priorArtifactPromise = priorPhaseArtifacts[artifactDefn.id] || Promise.resolve();
const artifactPromise = priorArtifactPromise.then(async () => {
const dependencies = isFinalPhase
? await collectArtifactDependencies(artifactDefn, artifactState.getArtifact)
: /** @type {Dependencies} */ ({});
const status = {
msg: `Getting artifact: ${artifactDefn.id}`,
id: `lh:gather:getArtifact:${artifactDefn.id}`,
};
if (isFinalPhase) {
log.time(status);
}
const artifact = await gatherer[phase]({
gatherMode,
driver,
page,
baseArtifacts,
dependencies,
computedCache,
settings,
});
if (isFinalPhase) {
log.timeEnd(status);
}
return artifact;
});
await artifactPromise.catch((err) => {
Sentry.captureException(err, {
tags: {gatherer: artifactDefn.id, phase},
level: 'error',
});
log.error(artifactDefn.id, err.message);
});
artifactState[phase][artifactDefn.id] = artifactPromise;
}
}
CLS を計算するには?
# env CHROME_PATH=/snap/bin/chromium lighthouse https://google.com/ --only-audits=cumulative-layout-shift --disable-full-page-screenshot -G --chrome-flags="--headless=new"
これによって、CLSに関するものだけ有効になる。これは何の Audit を必要としているか。
/**
* @fileoverview This metric represents the amount of visual shifting of DOM elements during page load.
*/
class CumulativeLayoutShift extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'cumulative-layout-shift',
title: str_(i18n.UIStrings.cumulativeLayoutShiftMetric),
description: str_(UIStrings.description),
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
requiredArtifacts: ['traces'],
};
}
traces を要求している。
--gather-mode (-G) で実行すると ./latest-run
の下に次のようなデータが生成されている。
$ tree latest-run
latest-run
├── artifacts.json
├── defaultPass.devtoolslog.json
└── defaultPass.trace.json
この、 defaultPass.trace.json が Chrome内部の トレーシングデータで、 chrome://tracing
ページでも食わせることができた。 devtoolslog よりはるかに細かいし、大きい。
$ la latest-run
.rw-r--r-- 6.2k mizchi 16 Feb 19:21 artifacts.json
.rw-r--r-- 9.0M mizchi 16 Feb 19:21 defaultPass.devtoolslog.json
.rw-r--r-- 104M mizchi 16 Feb 19:21 defaultPass.trace.json
CLS 計算処理の実体
core/computed/metrics/cumulative-layout-shift.js
なんか2種類の計算エンジンがあるが、一旦計算に使っているノードだけ見てみる
/**
* @param {LH.Trace} trace
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>, newEngineResult?: {cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}, newEngineResultDiffered: boolean}>}
*/
static async compute_(trace, context) {
// console.log("CLS: compute_", trace.traceEvents.length);
const processedTrace = await ProcessedTrace.request(trace, context);
console.log("CLS: compute_", {
frameEvents: processedTrace.frameEvents.length,
frameTreeEvents: processedTrace.frameTreeEvents.length,
processEvents: processedTrace.processEvents.length,
});
const allFrameShiftEvents =
CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
const impactByNodeId =
CumulativeLayoutShift.getImpactByNodeId(allFrameShiftEvents);
const mainFrameShiftEvents = allFrameShiftEvents.filter(
(e) => e.isMainFrame
);
const cumulativeLayoutShift =
CumulativeLayoutShift.calculate(allFrameShiftEvents);
const cumulativeLayoutShiftMainFrame =
CumulativeLayoutShift.calculate(mainFrameShiftEvents);
///...
// console.log("CLS: compute_:end");
console.log("CLS: compute_", {
allFrameShiftEvents: allFrameShiftEvents.length,
impactByNodeId: impactByNodeId.size,
mainFrameShiftEvents: mainFrameShiftEvents.length,
cumulativeLayoutShift,
// cumulativeLayoutShiftMainFrame,
newEngineResult,
});
console.log(JSON.stringify(mainFrameShiftEvents, null, 2));
return {
cumulativeLayoutShift,
cumulativeLayoutShiftMainFrame,
impactByNodeId,
newEngineResult,
newEngineResultDiffered,
};
CLS: compute_ {
allFrameShiftEvents: 4,
impactByNodeId: 8,
mainFrameShiftEvents: 3,
cumulativeLayoutShift: 0.05644913431254465,
newEngineResult: {
cumulativeLayoutShift: 0.05644913431254465,
cumulativeLayoutShiftMainFrame: 0.05644913431254465
}
}
[
{
"ts": 23739301316,
"isMainFrame": true,
"weightedScore": 0.001218699565871869,
"impactedNodes": [
{
"new_rect": [
17,
767,
151,
56
],
"node_id": 271,
"old_rect": [
17,
807,
151,
8
]
}
],
"event": {
"args": {
"data": {
"cumulative_score": 0.001218699565871869,
"frame_max_distance": 40.21875,
"had_recent_input": false,
"impacted_nodes": [
{
"new_rect": [
17,
767,
151,
56
],
"node_id": 271,
"old_rect": [
17,
807,
151,
8
]
}
],
"is_main_frame": true,
"last_input_timestamp": 183.69999999925494,
"overall_max_distance": 121.171875,
"region_rects": [
[
17,
767,
151,
56
]
],
"score": 0.001218699565871869,
"weighted_score_delta": 0.001218699565871869
},
"frame": "BB76A285A0B7E4E3846986E02A285FB6"
},
"cat": "loading",
"name": "LayoutShift",
"ph": "I",
"pid": 228159,
"s": "t",
"tid": 1,
"ts": 23739301316,
"tts": 1543314
}
},
{
"ts": 23739374660,
"isMainFrame": true,
"weightedScore": 0.01649647909556565,
"impactedNodes": [
{
"new_rect": [
8,
791,
396,
32
],
"node_id": 247,
"old_rect": [
8,
698,
396,
125
]
}
],
"event": {
"args": {
"data": {
"cumulative_score": 0.01771517866143752,
"frame_max_distance": 93,
"had_recent_input": false,
"impacted_nodes": [
{
"new_rect": [
8,
791,
396,
32
],
"node_id": 247,
"old_rect": [
8,
698,
396,
125
]
}
],
"is_main_frame": true,
"last_input_timestamp": 183.69999999925494,
"overall_max_distance": 121.171875,
"region_rects": [
[
8,
698,
396,
125
]
],
"score": 0.01649647909556565,
"weighted_score_delta": 0.01649647909556565
},
"frame": "BB76A285A0B7E4E3846986E02A285FB6"
},
"cat": "loading",
"name": "LayoutShift",
"ph": "I",
"pid": 228159,
"s": "t",
"tid": 1,
"ts": 23739374660,
"tts": 1613600
}
},
{
"ts": 23739526604,
"isMainFrame": true,
"weightedScore": 0.03873395565110713,
"impactedNodes": [
{
"new_rect": [
8,
288,
173,
95
],
"node_id": 196,
"old_rect": [
8,
295,
173,
88
]
},
{
"new_rect": [
193,
339,
174,
45
],
"node_id": 206,
"old_rect": [
0,
0,
0,
0
]
},
{
"new_rect": [
193,
339,
174,
31
],
"node_id": 209,
"old_rect": [
193,
339,
174,
31
]
},
{
"new_rect": [
379,
339,
26,
45
],
"node_id": 220,
"old_rect": [
0,
0,
0,
0
]
},
{
"new_rect": [
379,
339,
26,
31
],
"node_id": 223,
"old_rect": [
379,
339,
26,
31
]
}
],
"event": {
"args": {
"data": {
"cumulative_score": 0.05644913431254465,
"frame_max_distance": 424.96875,
"had_recent_input": false,
"impacted_nodes": [
{
"new_rect": [
8,
288,
173,
95
],
"node_id": 196,
"old_rect": [
8,
295,
173,
88
]
},
{
"new_rect": [
193,
339,
174,
45
],
"node_id": 206,
"old_rect": [
0,
0,
0,
0
]
},
{
"new_rect": [
193,
339,
174,
31
],
"node_id": 209,
"old_rect": [
193,
339,
174,
31
]
},
{
"new_rect": [
379,
339,
26,
45
],
"node_id": 220,
"old_rect": [
0,
0,
0,
0
]
},
{
"new_rect": [
379,
339,
26,
31
],
"node_id": 223,
"old_rect": [
379,
339,
26,
31
]
}
],
"is_main_frame": true,
"last_input_timestamp": 183.69999999925494,
"overall_max_distance": 424.96875,
"region_rects": [
[
8,
288,
173,
51
],
[
8,
339,
173,
44
],
[
193,
339,
174,
44
],
[
379,
339,
26,
44
],
[
193,
383,
174,
1
],
[
379,
383,
26,
1
]
],
"score": 0.03873395565110713,
"weighted_score_delta": 0.03873395565110713
},
"frame": "BB76A285A0B7E4E3846986E02A285FB6"
},
"cat": "loading",
"name": "LayoutShift",
"ph": "I",
"pid": 228159,
"s": "t",
"tid": 1,
"ts": 23739526604,
"tts": 1767404
}
}
計算処理
static calculate(layoutShiftEvents) {
const gapMicroseconds = 1_000_000;
const limitMicroseconds = 5_000_000;
let maxScore = 0;
let currentClusterScore = 0;
let firstTs = Number.NEGATIVE_INFINITY;
let prevTs = Number.NEGATIVE_INFINITY;
for (const event of layoutShiftEvents) {
if (
event.ts - firstTs > limitMicroseconds ||
event.ts - prevTs > gapMicroseconds
) {
firstTs = event.ts;
currentClusterScore = 0;
}
prevTs = event.ts;
currentClusterScore += event.weightedScore;
maxScore = Math.max(maxScore, currentClusterScore);
}
return maxScore;
}
タイムスタンプが離れすぎていない範囲で区切って、最も大きい LayoutShift イベントの weightedScore の合計
多分衝撃率を計算してると思うんだけど、なんでMaxなんだろう。
累積レイアウトシフトだから、累計じゃないのか?
Comuputed*.request() は同じ計算を二回やらないように結果をキャッシュしている
/**
* Decorate computableArtifact with a caching `request()` method which will
* automatically call `computableArtifact.compute_()` under the hood.
* @template {{name: string, compute_(dependencies: unknown, context: LH.Artifacts.ComputedContext): Promise<unknown>}} C
* @template {Array<keyof LH.Util.FirstParamType<C['compute_']>>} K
* @param {C} computableArtifact
* @param {(K & ([keyof LH.Util.FirstParamType<C['compute_']>] extends [K[number]] ? unknown : never)) | null} keys List of properties of `dependencies` used by `compute_`; other properties are filtered out. Use `null` to allow all properties. Ensures that only required properties are used for caching result.
*/
function makeComputedArtifact(computableArtifact, keys) {
// tsc (3.1) has more difficulty with template inter-references in jsdoc, so
// give types to params and return value the long way, essentially recreating
// polymorphic-this behavior for C.
/**
* Return an automatically cached result from the computed artifact.
* @param {LH.Util.FirstParamType<C['compute_']>} dependencies
* @param {LH.Artifacts.ComputedContext} context
* @return {ReturnType<C['compute_']>}
*/
const request = (dependencies, context) => {
const pickedDependencies = keys ?
Object.fromEntries(keys.map(key => [key, dependencies[key]])) :
dependencies;
// NOTE: break immutability solely for this caching-controller function.
const computedCache = /** @type {Map<string, ArbitraryEqualityMap>} */ (context.computedCache);
const computedName = computableArtifact.name;
const cache = computedCache.get(computedName) || new ArbitraryEqualityMap();
computedCache.set(computedName, cache);
/** @type {ReturnType<C['compute_']>|undefined} */
const computed = cache.get(pickedDependencies);
if (computed) {
return computed;
}
const status = {msg: `Computing artifact: ${computedName}`, id: `lh:computed:${computedName}`};
log.time(status, 'verbose');
const artifactPromise = /** @type {ReturnType<C['compute_']>} */
(computableArtifact.compute_(pickedDependencies, context));
cache.set(pickedDependencies, artifactPromise);
artifactPromise.then(() => log.timeEnd(status)).catch(() => log.timeEnd(status));
return artifactPromise;
};
return Object.assign(computableArtifact, {request});
}
CDPから Har を生成する