lighthouse読む
Lighthouse計測で様々な条件をシミュレーションしたい
$ git clone https://github.com/GoogleChrome/lighthouse
puppeteer を使って、CDPセッションに接続
- core/gather がデータを収集する処理
- core/gather/driver が chrome を操作する処理
/**
* Run Lighthouse.
* @param {string=} url The URL to test. Optional if running in auditMode.
* @param {LH.Flags=} flags Optional settings for the Lighthouse run. If present,
* they will override any settings in the config.
* @param {LH.Config=} config Configuration for the Lighthouse run. If
* not present, the default config is used.
* @param {LH.Puppeteer.Page=} page
* @return {Promise<LH.RunnerResult|undefined>}
*/
async function lighthouse(url, flags = {}, config, page) {
return navigation(page, url, {config, flags});
}
navigation を呼ぶ
/**
* @param {LH.Puppeteer.Page|undefined} page
* @param {LH.NavigationRequestor|undefined} requestor
* @param {{config?: LH.Config, flags?: LH.Flags}} [options]
* @return {Promise<LH.RunnerResult|undefined>}
*/
async function navigation(page, requestor, options) {
const gatherResult = await navigationGather(page, requestor, options);
return Runner.audit(gatherResult.artifacts, gatherResult.runnerOptions);
}
navigationGather
/**
* @param {LH.Puppeteer.Page|undefined} page
* @param {LH.NavigationRequestor|undefined} requestor
* @param {{config?: LH.Config, flags?: LH.Flags}} [options]
* @return {Promise<LH.Gatherer.GatherResult>}
*/
async function navigationGather(page, requestor, options = {}) {
const {flags = {}, config} = options;
log.setLevel(flags.logLevel || 'error');
const {resolvedConfig} = await initializeConfig('navigation', config, flags);
const computedCache = new Map();
const isCallback = typeof requestor === 'function';
const runnerOptions = {resolvedConfig, computedCache};
const gatherFn = async () => {
const normalizedRequestor = isCallback ? requestor : UrlUtils.normalizeUrl(requestor);
/** @type {LH.Puppeteer.Browser|undefined} */
let lhBrowser = undefined;
/** @type {LH.Puppeteer.Page|undefined} */
let lhPage = undefined;
// For navigation mode, we shouldn't connect to a browser in audit mode,
// therefore we connect to the browser in the gatherFn callback.
if (!page) {
const {hostname = DEFAULT_HOSTNAME, port = DEFAULT_PORT} = flags;
lhBrowser = await puppeteer.connect({browserURL: `http://${hostname}:${port}`, defaultViewport: null});
lhPage = await lhBrowser.newPage();
page = lhPage;
}
const driver = new Driver(page);
const context = {
driver,
lhBrowser,
lhPage,
page,
resolvedConfig,
requestor: normalizedRequestor,
computedCache,
};
const {baseArtifacts} = await _setup(context);
const artifacts = await _navigation({...context, baseArtifacts});
await _cleanup(context);
return finalizeArtifacts(baseArtifacts, artifacts);
};
const artifacts = await Runner.gather(gatherFn, runnerOptions);
return {artifacts, runnerOptions};
}
- requestor が関数ならば実行してURL先を確定
- 未設定ならば lighthouse.Page を初期化している。
- Lighthouse の実行コンテキストを作っている
- driver を初期化
-
_setup
に driver を渡して初期化
navigation > new Driver(page)
Driver 初期化自体では何も初期化しない
/** @implements {LH.Gatherer.Driver} */
class Driver {
/**
* @param {LH.Puppeteer.Page} page
*/
constructor(page) {
this._page = page;
/** @type {TargetManager|undefined} */
this._targetManager = undefined;
/** @type {NetworkMonitor|undefined} */
this._networkMonitor = undefined;
/** @type {ExecutionContext|undefined} */
this._executionContext = undefined;
/** @type {Fetcher|undefined} */
this._fetcher = undefined;
this.defaultSession = throwingSession;
}
connect を呼ばれると、driver のメンバが初期化される(まだ呼ばれてない)
/** @return {Promise<void>} */
async connect() {
if (this.defaultSession !== throwingSession) return;
const status = {msg: 'Connecting to browser', id: 'lh:driver:connect'};
log.time(status);
const cdpSession = await this._page.target().createCDPSession();
this._targetManager = new TargetManager(cdpSession);
await this._targetManager.enable();
this._networkMonitor = new NetworkMonitor(this._targetManager);
await this._networkMonitor.enable();
this.defaultSession = this._targetManager.rootSession();
this._executionContext = new ExecutionContext(this.defaultSession);
this._fetcher = new Fetcher(this.defaultSession);
log.timeEnd(status);
}
- cdpSession を作る
- TargetManager に渡す
// navigationGatherer
const {baseArtifacts} = await _setup(context);
/**
* @param {{driver: Driver, resolvedConfig: LH.Config.ResolvedConfig, requestor: LH.NavigationRequestor}} args
* @return {Promise<{baseArtifacts: LH.BaseArtifacts}>}
*/
async function _setup({driver, resolvedConfig, requestor}) {
await driver.connect();
// 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();
}
const baseArtifacts = await getBaseArtifacts(resolvedConfig, driver, {gatherMode: 'navigation'});
const {warnings} =
await prepare.prepareTargetForNavigationMode(driver, resolvedConfig.settings, requestor);
baseArtifacts.LighthouseRunWarnings.push(...warnings);
return {baseArtifacts};
}
BaseArtifacts と TargetManager が何なのかを調べる
navigationGather > Driver > TargetManager
/**
* Tracks targets (the page itself, its iframes, their iframes, etc) as they
* appear and allows listeners to the flattened protocol events from all targets.
*/
class TargetManager extends ProtocolEventEmitter {
/** @param {LH.Puppeteer.CDPSession} cdpSession */
constructor(cdpSession) {
super();
this._enabled = false;
this._rootCdpSession = cdpSession;
this._mainFrameId = '';
/**
* A map of target id to target/session information. Used to ensure unique
* attached targets.
* @type {Map<string, TargetWithSession>}
*/
this._targetIdToTargets = new Map();
/** @type {Map<string, LH.Crdp.Runtime.ExecutionContextDescription>} */
this._executionContextIdToDescriptions = new Map();
this._onSessionAttached = this._onSessionAttached.bind(this);
this._onFrameNavigated = this._onFrameNavigated.bind(this);
this._onExecutionContextCreated = this._onExecutionContextCreated.bind(this);
this._onExecutionContextDestroyed = this._onExecutionContextDestroyed.bind(this);
this._onExecutionContextsCleared = this._onExecutionContextsCleared.bind(this);
}
どうやら、CDPを直接ハンドルするリスナーを定義している
/**
* @param {LH.Crdp.Page.FrameNavigatedEvent} frameNavigatedEvent
*/
async _onFrameNavigated(frameNavigatedEvent) {
// Child frames are handled in `_onSessionAttached`.
if (frameNavigatedEvent.frame.parentId) return;
if (!this._enabled) return;
// It's not entirely clear when this is necessary, but when the page switches processes on
// navigating from about:blank to the `requestedUrl`, resetting `setAutoAttach` has been
// necessary in the past.
try {
await this._rootCdpSession.send('Target.setAutoAttach', {
autoAttach: true,
flatten: true,
waitForDebuggerOnStart: true,
});
} catch (err) {
// The page can be closed at the end of the run before this CDP function returns.
// In these cases, just ignore the error since we won't need the page anyway.
if (this._enabled) throw err;
}
}
Target は計測対象のセッションに接続したり、デタッチしている?
ここの onProtocolEvent で全部のイベントを監視するリスナーが設定されている
/**
* Returns a listener for all protocol events from session, and augments the
* event with the sessionId.
* @param {LH.Protocol.TargetType} targetType
* @param {string} sessionId
*/
_getProtocolEventListener(targetType, sessionId) {
/**
* @template {keyof LH.Protocol.RawEventMessageRecord} EventName
* @param {EventName} method
* @param {LH.Protocol.RawEventMessageRecord[EventName]['params']} params
*/
const onProtocolEvent = (method, params) => {
// Cast because tsc 4.7 still can't quite track the dependent parameters.
const payload = /** @type {LH.Protocol.RawEventMessage} */ (
{method, params, targetType, sessionId});
this.emit('protocolevent', payload);
};
return onProtocolEvent;
}
const trueProtocolListener = this._getProtocolEventListener(targetType, newSession.id());
/** @type {(event: unknown) => void} */
// @ts-expect-error - pptr currently typed only for single arg emits.
const protocolListener = trueProtocolListener;
cdpSession.on('*', protocolListener);
cdpSession.on('sessionattached', this._onSessionAttached);
TargetManager 自体がイベントリスナーなので、これを監視することでCDPSession全イベントが取れるみたいだ
Session とは
/**
* @param {LH.Puppeteer.CDPSession} cdpSession
*/
async _onSessionAttached(cdpSession) {
const newSession = new ProtocolSession(cdpSession);
let targetType;
try {
const {targetInfo} = await newSession.sendCommand('Target.getTargetInfo');
targetType = targetInfo.type;
core/gather/session.js
cdpSession のラッパー
/**
* Puppeteer timeouts must fit into an int32 and the maximum timeout for `setTimeout` is a *signed*
* int32. However, this also needs to account for the puppeteer buffer we add to the timeout later.
*
* So this is defined as the max *signed* int32 minus PPTR_BUFFER.
*
* In human terms, this timeout is ~25 days which is as good as infinity for all practical purposes.
*/
const MAX_TIMEOUT = 2147483647 - PPTR_BUFFER;
/** @typedef {LH.Protocol.StrictEventEmitterClass<LH.CrdpEvents>} CrdpEventMessageEmitter */
const CrdpEventEmitter = /** @type {CrdpEventMessageEmitter} */ (EventEmitter);
/** @implements {LH.Gatherer.ProtocolSession} */
class ProtocolSession extends CrdpEventEmitter {
/**
* @param {LH.Puppeteer.CDPSession} cdpSession
*/
constructor(cdpSession) {
super();
Lantern
Project Lantern は、ページ アクティビティをモデル化し、ブラウザーの実行をシミュレートすることで、Lighthouse の実行時間を短縮し、監査の品質を向上させるための継続的な取り組みです。このドキュメントでは、これらのモデルの精度について詳しく説明し、予想される自然な変動性を把握します。
TargetManager
targetManager.enable() で cdp を初期化する
/**
* @return {Promise<void>}
*/
async enable() {
if (this._enabled) return;
this._enabled = true;
this._targetIdToTargets = new Map();
this._executionContextIdToDescriptions = new Map();
this._rootCdpSession.on('Page.frameNavigated', this._onFrameNavigated);
this._rootCdpSession.on('Runtime.executionContextCreated', this._onExecutionContextCreated);
this._rootCdpSession.on('Runtime.executionContextDestroyed', this._onExecutionContextDestroyed);
this._rootCdpSession.on('Runtime.executionContextsCleared', this._onExecutionContextsCleared);
await this._rootCdpSession.send('Page.enable');
await this._rootCdpSession.send('Runtime.enable');
this._mainFrameId = (await this._rootCdpSession.send('Page.getFrameTree')).frameTree.frame.id;
// Start with the already attached root session.
await this._onSessionAttached(this._rootCdpSession);
}
onSessionnAttached
/**
* @param {LH.Puppeteer.CDPSession} cdpSession
*/
async _onSessionAttached(cdpSession) {
const newSession = new ProtocolSession(cdpSession);
// ...
class ProtocolSession
/** @implements {LH.Gatherer.ProtocolSession} */
class ProtocolSession extends CrdpEventEmitter {
/**
* @param {LH.Puppeteer.CDPSession} cdpSession
*/
constructor(cdpSession) {
super();
this._cdpSession = cdpSession;
/** @type {LH.Crdp.Target.TargetInfo|undefined} */
this._targetInfo = undefined;
/** @type {number|undefined} */
this._nextProtocolTimeout = undefined;
this._handleProtocolEvent = this._handleProtocolEvent.bind(this);
// @ts-expect-error Puppeteer expects the handler params to be type `unknown`
this._cdpSession.on('*', this._handleProtocolEvent);
// If the target crashes, we can't continue gathering.
// FWIW, if the target unexpectedly detaches (eg the user closed the tab), pptr will
// catch that and reject in this._cdpSession.send, which is caught by us.
/** @param {Error} _ */
let rej = _ => {}; // Poor man's Promise.withResolvers()
this._targetCrashedPromise = /** @type {Promise<never>} */ (
new Promise((_, theRej) => rej = theRej));
this.on('Inspector.targetCrashed', async () => {
log.error('TargetManager', 'Inspector.targetCrashed');
// Manually detach so no more CDP traffic is attempted.
// Don't await, else our rejection will be a 'Target closed' protocol error on cross-talk
// CDP calls.
void this.dispose();
rej(new LighthouseError(LighthouseError.errors.TARGET_CRASHED));
});
}
クラッシュレポーターを差し込みつつ、ラップしている。
多分重要なのは sendCommand
/**
* @template {keyof LH.CrdpCommands} C
* @param {C} method
* @param {LH.CrdpCommands[C]['paramsType']} params
* @return {Promise<LH.CrdpCommands[C]['returnType']>}
*/
sendCommand(method, ...params) {
const timeoutMs = this.getNextProtocolTimeout();
this._nextProtocolTimeout = undefined;
/** @type {NodeJS.Timeout|undefined} */
let timeout;
const timeoutPromise = new Promise((resolve, reject) => {
// Unexpected setTimeout invocation to preserve the error stack. https://github.com/GoogleChrome/lighthouse/issues/13332
// eslint-disable-next-line max-len
timeout = setTimeout(reject, timeoutMs, new LighthouseError(LighthouseError.errors.PROTOCOL_TIMEOUT, {
protocolMethod: method,
}));
});
const resultPromise = this._cdpSession.send(method, ...params, {
// Add 50ms to the Puppeteer timeout to ensure the Lighthouse timeout finishes first.
timeout: timeoutMs + PPTR_BUFFER,
}).catch((error) => {
log.formatProtocol('method <= browser ERR', {method}, 'error');
throw LighthouseError.fromProtocolMessage(method, error);
});
const resultWithTimeoutPromise =
Promise.race([resultPromise, timeoutPromise, this._targetCrashedPromise]);
return resultWithTimeoutPromise.finally(() => {
if (timeout) clearTimeout(timeout);
});
}
実態は cdp.send だが、タイムアウト付きでラップしている
ここまでに得た知識で、lighthouse と同等の初期化処理でCDPに直接アクセスしてみる。
import puppeteer from "puppeteer";
import * as chromeLauncher from "chrome-launcher";
const chrome = await chromeLauncher.launch({
userDataDir: false,
chromeFlags: ["--no-sandbox"],
chromePath: "/snap/bin/chromium",
});
const browser = await puppeteer.connect({
browserURL: `http://localhost:${chrome.port}`,
defaultViewport: null,
});
const page = await browser.newPage();
const cdp = await page.createCDPSession();
const conn = cdp.connection()!;
// hooks
cdp.on("*", (...args) => {
console.log("[cdp]", ...args);
});
conn.on("*", (...args) => {
console.log("[conn]", ...args);
});
// send
await cdp.send("Target.setAutoAttach", {
autoAttach: true,
flatten: true,
waitForDebuggerOnStart: true,
});
await cdp.send("Page.enable");
await cdp.send("Runtime.enable");
const frameTree = await cdp.send("Page.getFrameTree");
console.log("mainId", frameTree.frameTree.frame);
await page.goto("https://github.com", {
waitUntil: "networkidle2",
});
const res = await cdp.send("Target.getTargetInfo");
console.log(res);
結果
$ node script/02-run.ts
[cdp] Runtime.executionContextCreated {
context: {
id: 2,
origin: '',
name: '__puppeteer_utility_world__24.2.1',
uniqueId: '-1957335987268521367.7472334966428414066',
auxData: {
isDefault: false,
type: 'isolated',
frameId: '95D444464DC5FA3BF51F80FC5400C38D'
}
}
}
[cdp] Runtime.executionContextCreated {
context: {
id: 1,
origin: '://',
name: '',
uniqueId: '-8097433944122872796.-8927146037938287057',
auxData: {
isDefault: true,
type: 'default',
frameId: '95D444464DC5FA3BF51F80FC5400C38D'
}
}
}
mainId {
id: '95D444464DC5FA3BF51F80FC5400C38D',
loaderId: 'C599BE1BBE44CB7E4E4DFF7B54D9F9EE',
url: 'about:blank',
domainAndRegistry: '',
securityOrigin: '://',
mimeType: 'text/html',
adFrameStatus: { adFrameType: 'none' },
secureContextType: 'InsecureScheme',
crossOriginIsolatedContextType: 'NotIsolated',
gatedAPIFeatures: []
}
[cdp] Page.frameStartedLoading { frameId: '95D444464DC5FA3BF51F80FC5400C38D' }
[cdp] Runtime.executionContextsCleared {}
[cdp] Page.frameNavigated {
frame: {
id: '95D444464DC5FA3BF51F80FC5400C38D',
loaderId: '0FD4137EE31A1580DDE74D5A00FD46BA',
url: 'https://github.com/',
domainAndRegistry: 'github.com',
securityOrigin: 'https://github.com',
mimeType: 'text/html',
adFrameStatus: { adFrameType: 'none' },
secureContextType: 'Secure',
crossOriginIsolatedContextType: 'NotIsolated',
gatedAPIFeatures: []
},
type: 'Navigation'
}
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: '95D444464DC5FA3BF51F80FC5400C38D',
type: 'page',
title: 'github.com',
url: 'https://github.com/',
attached: true,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[cdp] Runtime.executionContextCreated {
context: {
id: 3,
origin: 'https://github.com',
name: '',
uniqueId: '2024160153867014494.-7013613868782907409',
auxData: {
isDefault: true,
type: 'default',
frameId: '95D444464DC5FA3BF51F80FC5400C38D'
}
}
}
[cdp] Runtime.executionContextCreated {
context: {
id: 4,
origin: '://',
name: '__puppeteer_utility_world__24.2.1',
uniqueId: '-1813183885552145059.293750994607843554',
auxData: {
isDefault: false,
type: 'isolated',
frameId: '95D444464DC5FA3BF51F80FC5400C38D'
}
}
}
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: '95D444464DC5FA3BF51F80FC5400C38D',
type: 'page',
title: 'GitHub · Build and ship software on a single, collaborative platform · GitHub',
url: 'https://github.com/',
attached: true,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: '95D444464DC5FA3BF51F80FC5400C38D',
type: 'page',
title: 'GitHub · Build and ship software on a single, collaborative platform · GitHub',
url: 'https://github.com/',
attached: true,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[cdp] Page.navigatedWithinDocument {
frameId: '95D444464DC5FA3BF51F80FC5400C38D',
url: 'https://github.com/',
navigationType: 'historyApi'
}
[cdp] Page.navigatedWithinDocument {
frameId: '95D444464DC5FA3BF51F80FC5400C38D',
url: 'https://github.com/',
navigationType: 'historyApi'
}
[cdp] Page.domContentEventFired { timestamp: 296366.681765 }
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: '95D444464DC5FA3BF51F80FC5400C38D',
type: 'page',
title: 'GitHub · Build and ship software on a single, collaborative platform · GitHub',
url: 'https://github.com/',
attached: true,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[cdp] Page.navigatedWithinDocument {
frameId: '95D444464DC5FA3BF51F80FC5400C38D',
url: 'https://github.com/',
navigationType: 'historyApi'
}
[cdp] Page.loadEventFired { timestamp: 296366.898252 }
[cdp] Page.frameStoppedLoading { frameId: '95D444464DC5FA3BF51F80FC5400C38D' }
{
targetInfo: {
targetId: '95D444464DC5FA3BF51F80FC5400C38D',
type: 'page',
title: 'GitHub · Build and ship software on a single, collaborative platform · GitHub',
url: 'https://github.com/',
attached: true,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[conn] sessiondetached CdpCDPSession {}
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: '95D444464DC5FA3BF51F80FC5400C38D',
type: 'page',
title: 'GitHub · Build and ship software on a single, collaborative platform · GitHub',
url: 'https://github.com/',
attached: false,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[cdp] Symbol(CDPSession.Disconnected) undefined
[conn] sessiondetached CdpCDPSession {}
[conn] Target.detachedFromTarget {
sessionId: 'A83C7CE4B7173FF7E1E2F10B1CBB98D7',
targetId: '95D444464DC5FA3BF51F80FC5400C38D'
}
[conn] Target.targetDestroyed { targetId: '95D444464DC5FA3BF51F80FC5400C38D' }
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: '90761FADCC4C5126FFC1B3A4F1DCCB21',
type: 'tab',
title: '',
url: '',
attached: false,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[conn] sessiondetached CdpCDPSession {}
[conn] Target.detachedFromTarget {
sessionId: '0CC4723A8E46D35A6B17CB4F6DA19EA0',
targetId: '90761FADCC4C5126FFC1B3A4F1DCCB21'
}
[conn] Target.targetDestroyed { targetId: '90761FADCC4C5126FFC1B3A4F1DCCB21' }
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: '6A598B86AC255479555BE92FBE4D9DE0',
type: 'page',
title: 'about:blank',
url: 'about:blank',
attached: false,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[conn] sessiondetached CdpCDPSession {}
[conn] Target.targetDestroyed { targetId: '6A598B86AC255479555BE92FBE4D9DE0' }
[conn] Target.targetInfoChanged {
targetInfo: {
targetId: 'D55134CF007137F13ABF7DC689D2366D',
type: 'tab',
title: '',
url: '',
attached: false,
canAccessOpener: false,
browserContextId: '3C42CF0F6F6E7B157BE26304344BF956'
}
}
[conn] sessiondetached CdpCDPSession {}
[conn] Target.detachedFromTarget {
sessionId: '4D36A7402414F79ADFD8570635B65095',
targetId: 'D55134CF007137F13ABF7DC689D2366D'
}
[conn] Target.targetDestroyed { targetId: 'D55134CF007137F13ABF7DC689D2366D' }
[conn] Symbol(CDPSession.Disconnected) undefine
ここまでまだ driver.connect() を見ている途中だった
/** @return {Promise<void>} */
async connect() {
if (this.defaultSession !== throwingSession) return;
const status = {msg: 'Connecting to browser', id: 'lh:driver:connect'};
log.time(status);
const cdpSession = await this._page.target().createCDPSession();
this._targetManager = new TargetManager(cdpSession);
await this._targetManager.enable();
この先にいく
this._networkMonitor = new NetworkMonitor(this._targetManager);
await this._networkMonitor.enable();
this.defaultSession = this._targetManager.rootSession();
this._executionContext = new ExecutionContext(this.defaultSession);
this._fetcher = new Fetcher(this.defaultSession);
log.timeEnd(status);
- networkMonitor.enable
- new ExecutionContext
- new Fetcher
NetwokrMonitor
/**
* @fileoverview This class wires up the procotol to a network recorder and provides overall
* status inspection state.
*/
class NetworkMonitor extends NetworkMonitorEventEmitter {
/** @type {NetworkRecorder|undefined} */
_networkRecorder = undefined;
/** @type {Array<LH.Crdp.Page.Frame>} */
_frameNavigations = [];
/** @param {LH.Gatherer.Driver['targetManager']} targetManager */
constructor(targetManager) {
super();
/** @type {LH.Gatherer.Driver['targetManager']} */
this._targetManager = targetManager;
/** @type {LH.Gatherer.ProtocolSession} */
this._session = targetManager.rootSession();
/** @param {LH.Crdp.Page.FrameNavigatedEvent} event */
this._onFrameNavigated = event => this._frameNavigations.push(event.frame);
/** @param {LH.Protocol.RawEventMessage} event */
this._onProtocolMessage = event => {
if (!this._networkRecorder) return;
this._networkRecorder.dispatch(event);
};
}
/**
* @return {Promise<void>}
*/
async enable() {
if (this._networkRecorder) return;
this._frameNavigations = [];
this._networkRecorder = new NetworkRecorder();
/**
* Reemit the same network recorder events.
* @param {keyof NetworkRecorderEventMap} event
* @return {(r: NetworkRequest) => void}
*/
const reEmit = event => r => {
this.emit(event, r);
this._emitNetworkStatus();
};
this._networkRecorder.on('requeststarted', reEmit('requeststarted'));
this._networkRecorder.on('requestfinished', reEmit('requestfinished'));
this._session.on('Page.frameNavigated', this._onFrameNavigated);
this._targetManager.on('protocolevent', this._onProtocolMessage);
}
NetworkRecorder を初期化していて、ハンドラを設定して、一部のイベントを再発火。
NetworkRecorder
class NetworkRecorder extends RequestEventEmitter {
/**
* Creates an instance of NetworkRecorder.
*/
constructor() {
super();
/** @type {NetworkRequest[]} */
this._records = [];
/** @type {Map<string, NetworkRequest>} */
this._recordsById = new Map();
}
結局ほぼ requeststarted と requestfinishide を横流ししているだけ
ExectuteContext
class ExecutionContext {
/** @param {LH.Gatherer.ProtocolSession} session */
constructor(session) {
this._session = session;
/** @type {number|undefined} */
this._executionContextId = undefined;
/**
* Marks how many execution context ids have been created, for purposes of having a unique
* value (that doesn't expose the actual execution context id) to
* use for __lighthouseExecutionContextUniqueIdentifier.
* @type {number}
*/
this._executionContextIdentifiersCreated = 0;
// We use isolated execution contexts for `evaluateAsync` that can be destroyed through navigation
// and other page actions. Cleanup our relevant bookkeeping as we see those events.
// Domains are enabled when a dedicated execution context is requested.
session.on("Page.frameNavigated", () => this.clearContextId());
session.on("Runtime.executionContextDestroyed", (event) => {
if (event.executionContextId === this._executionContextId) {
this.clearContextId();
}
});
}
page.evaluate()
相当っぽい
Fetcher
/**
* @fileoverview Fetcher is a utility for making requests to any arbitrary resource,
* ignoring normal browser constraints such as CORS.
*/
class Fetcher {
/**
* @param {LH.Gatherer.ProtocolSession} session
*/
constructor(session) {
this.session = session;
}
/**
* Fetches any resource using the network directly.
*
* @param {string} url
* @param {{timeout: number}=} options timeout is in ms
* @return {Promise<FetchResponse>}
*/
async fetchResource(url, options = {timeout: 2_000}) {
// In Lightrider, `Network.loadNetworkResource` is not implemented, but fetch
// is configured to work for any resource.
if (global.isLightrider) {
return this._wrapWithTimeout(this._fetchWithFetchApi(url), options.timeout);
}
return this._fetchResourceOverProtocol(url, options);
}
//....
/**
* @param {string} url
* @param {{timeout: number}} options timeout is in ms
* @return {Promise<FetchResponse>}
*/
async _fetchResourceOverProtocol(url, options) {
const startTime = Date.now();
const response = await this._wrapWithTimeout(this._loadNetworkResource(url), options.timeout);
const isOk = response.status && response.status >= 200 && response.status <= 299;
if (!response.stream || !isOk) return {status: response.status, content: null};
const timeout = options.timeout - (Date.now() - startTime);
const content = await this._readIOStream(response.stream, {timeout});
return {status: response.status, content};
}
_loadNetworkResource
が実体
/**
* @param {string} url
* @return {Promise<{stream: LH.Crdp.IO.StreamHandle|null, status: number|null}>}
*/
async _loadNetworkResource(url) {
const frameTreeResponse = await this.session.sendCommand('Page.getFrameTree');
const networkResponse = await this.session.sendCommand('Network.loadNetworkResource', {
frameId: frameTreeResponse.frameTree.frame.id,
url,
options: {
disableCache: true,
includeCredentials: true,
},
});
return {
stream: networkResponse.resource.success ? (networkResponse.resource.stream || null) : null,
status: networkResponse.resource.httpStatusCode || null,
};
}
この処理を真似ると、実際のセッションを処理とは無関係にネットワークリクエストを発行できそうにみえる。
やってみた
import puppeteer from "puppeteer";
import * as chromeLauncher from "chrome-launcher";
const chrome = await chromeLauncher.launch({
userDataDir: false,
chromeFlags: ["--no-sandbox"],
chromePath: "/snap/bin/chromium",
});
const browser = await puppeteer.connect({
browserURL: `http://localhost:${chrome.port}`,
defaultViewport: null,
});
const page = await browser.newPage();
const cdp = await page.createCDPSession();
await cdp.send("Page.enable");
await cdp.send("Network.enable");
await cdp.send("Runtime.enable");
await page.goto("https://github.com", {
waitUntil: "networkidle2",
});
const frameTreeResponse = await cdp.send("Page.getFrameTree");
const reqUrl = "https://jsonplaceholder.typicode.com/todos/1";
const networkResponse = await cdp.send("Network.loadNetworkResource", {
frameId: frameTreeResponse.frameTree.frame.id,
url: reqUrl,
options: {
disableCache: true,
includeCredentials: true,
},
});
console.log("req", networkResponse);
$ node script/04-req.ts
req {
resource: {
success: true,
httpStatusCode: 200,
stream: '1',
headers: {
'access-control-allow-credentials': 'true',
age: '22495',
'alt-svc': 'h3=":443"; ma=86400',
'cache-control': 'max-age=43200',
'cf-cache-status': 'HIT',
'cf-ray': '9125ca6cc9a9d791-NRT',
'content-encoding': 'zstd',
'content-type': 'application/json; charset=utf-8',
date: 'Sat, 15 Feb 2025 13:54:35 GMT',
etag: 'W/"53-hfEnumeNh6YirfjyjaujcOPPT+s"',
expires: '-1',
nel: '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}',
pragma: 'no-cache',
'report-to': '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1735436050&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=J1iPYscdGSQTZP%2FIp%2BrdPCguI3kwdYvzxKny%2FGIiv%2Bw%3D"}]}',
'reporting-endpoints': 'heroku-nel=https://nel.heroku.com/reports?ts=1735436050&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=J1iPYscdGSQTZP%2FIp%2BrdPCguI3kwdYvzxKny%2FGIiv%2Bw%3D',
server: 'cloudflare',
'server-timing': 'cfL4;desc="?proto=TCP&rtt=3309&min_rtt=2699&rtt_var=935&sent=8&recv=10&lost=0&retrans=0&sent_bytes=4016&recv_bytes=2252&delivery_rate=1230594&cwnd=253&unsent_bytes=0&cid=b8edc79947bf92d2&ts=19&x=0"',
vary: 'Origin, Accept-Encoding',
via: '1.1 vegur',
'x-content-type-options': 'nosniff',
'x-powered-by': 'Express',
'x-ratelimit-limit': '1000',
'x-ratelimit-remaining': '999',
'x-ratelimit-reset': '1735436088'
}
}
}
NavigationRunner _setup
ここまでで driver.connect() の処理が終了。
/**
* @param {{driver: Driver, resolvedConfig: LH.Config.ResolvedConfig, requestor: LH.NavigationRequestor}} args
* @return {Promise<{baseArtifacts: LH.BaseArtifacts}>}
*/
async function _setup({driver, resolvedConfig, requestor}) {
await driver.connect();
// 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();
}
const baseArtifacts = await getBaseArtifacts(resolvedConfig, driver, {gatherMode: 'navigation'});
const {warnings} =
await prepare.prepareTargetForNavigationMode(driver, resolvedConfig.settings, requestor);
baseArtifacts.LighthouseRunWarnings.push(...warnings);
return {baseArtifacts};
}
- gotoURL() でページをロードする
- ベースアーティファクトを取得
navigation
/**
* Navigates to the given URL, assuming that the page is not already on this URL.
* Resolves on the url of the loaded page, taking into account any redirects.
* Typical use of this method involves navigating to a neutral page such as `about:blank` in between
* navigations.
*
* @param {LH.Gatherer.Driver} driver
* @param {LH.NavigationRequestor} requestor
* @param {NavigationOptions} options
* @return {Promise<{requestedUrl: string, mainDocumentUrl: string, warnings: Array<LH.IcuMessage>}>}
*/
async function gotoURL(driver, requestor, options) {
const status = typeof requestor === 'string' ?
{msg: `Navigating to ${requestor}`, id: 'lh:driver:navigate'} :
{msg: 'Navigating using a user defined function', id: 'lh:driver:navigate'};
log.time(status);
const session = driver.defaultSession;
const networkMonitor = driver.networkMonitor;
// Enable the events and network monitor needed to track navigation progress.
await session.sendCommand('Page.enable');
await session.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});
let waitForNavigationTriggered;
if (typeof requestor === 'string') {
// No timeout needed for Page.navigate. See https://github.com/GoogleChrome/lighthouse/pull/6413
session.setNextProtocolTimeout(Infinity);
waitForNavigationTriggered = session.sendCommand('Page.navigate', {url: requestor});
} else {
waitForNavigationTriggered = requestor();
}
const waitForNavigated = options.waitUntil.includes('navigated');
const waitForLoad = options.waitUntil.includes('load');
const waitForFcp = options.waitUntil.includes('fcp');
/** @type {Array<Promise<{timedOut: boolean}>>} */
const waitConditionPromises = [];
if (waitForNavigated) {
const navigatedPromise = waitForFrameNavigated(session).promise;
waitConditionPromises.push(navigatedPromise.then(() => ({timedOut: false})));
}
if (waitForLoad) {
const waitOptions = resolveWaitForFullyLoadedOptions(options);
waitConditionPromises.push(waitForFullyLoaded(session, networkMonitor, waitOptions));
} else if (waitForFcp) {
throw new Error('Cannot wait for FCP without waiting for page load');
}
const waitConditions = await Promise.race([
session.onCrashPromise(),
Promise.all(waitConditionPromises),
]);
const timedOut = waitConditions.some(condition => condition.timedOut);
const navigationUrls = await networkMonitor.getNavigationUrls();
let requestedUrl = navigationUrls.requestedUrl;
if (typeof requestor === 'string') {
if (requestedUrl && !UrlUtils.equalWithExcludedFragments(requestor, requestedUrl)) {
log.error(
'Navigation',
`Provided URL (${requestor}) did not match initial navigation URL (${requestedUrl})`
);
}
requestedUrl = requestor;
}
if (!requestedUrl) throw Error('No navigations detected when running user defined requestor.');
const mainDocumentUrl = navigationUrls.mainDocumentUrl || requestedUrl;
// Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739.
await waitForNavigationTriggered;
if (options.debugNavigation) {
await waitForUserToContinue(driver);
}
log.timeEnd(status);
return {
requestedUrl,
mainDocumentUrl,
warnings: getNavigationWarnings({timedOut, mainDocumentUrl, requestedUrl}),
};
}
await session.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true})
は何が取れるようになる?
enable にするとこういうデータが取れた。
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'DOMContentLoaded',
timestamp: 299405.805636
}
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'firstMeaningfulPaintCandidate',
timestamp: 299405.875966
}
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'firstPaint',
timestamp: 299405.875966
}
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'firstMeaningfulPaintCandidate',
timestamp: 299405.875966
}
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'firstContentfulPaint',
times
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'firstImagePaint',
timestamp: 299406.075167
}
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'networkAlmostIdle',
timestamp: 299406.498319
}
[cdp] Page.lifecycleEvent {
frameId: '0E4E40F5368C6780E86C7B5FDF48036C',
loaderId: '265A206D552C3B597EF26B1F4AE80157',
name: 'firstMeaningfulPaint',
timestamp: 299405.875966
}
LCPはとれないがFCPやFMPが取れている
要は page.goto()
相当で、waitFor 等も自分でやってるみたいだった。
自分でも動かしてみたら、確かに遷移する
import puppeteer from "puppeteer";
import * as chromeLauncher from "chrome-launcher";
const chrome = await chromeLauncher.launch({
userDataDir: false,
chromeFlags: ["--no-sandbox"],
chromePath: "/snap/bin/chromium",
});
const browser = await puppeteer.connect({
browserURL: `http://localhost:${chrome.port}`,
defaultViewport: null,
});
const page = await browser.newPage();
const cdp = await page.createCDPSession();
await cdp.send("Page.enable");
await cdp.send("Network.enable");
await cdp.send("Runtime.enable");
const url = "https://github.com";
await cdp.send("Page.navigate", { url });
雰囲気をわかってきた。
puppeteer からCDPセッションはもらいたいが、基本的に puppeteer に依存したくない。
これもしかして、依存から puppeteer を剥がそうとしている?
core/gather/base-artifacts.js
/**
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @param {LH.Gatherer.Driver} driver
* @param {{gatherMode: LH.Gatherer.GatherMode}} context
* @return {Promise<LH.BaseArtifacts>}
*/
async function getBaseArtifacts(resolvedConfig, driver, context) {
const BenchmarkIndex = await getBenchmarkIndex(driver.executionContext);
const {userAgent, product} = await getBrowserVersion(driver.defaultSession);
return {
// Meta artifacts.
fetchTime: new Date().toJSON(),
Timing: [],
LighthouseRunWarnings: [],
settings: resolvedConfig.settings,
// Environment artifacts that can always be computed.
BenchmarkIndex,
HostUserAgent: userAgent,
HostFormFactor: userAgent.includes('Android') || userAgent.includes('Mobile') ?
'mobile' : 'desktop',
HostProduct: product,
// Contextual artifacts whose collection changes based on gather mode.
URL: {
finalDisplayedUrl: '',
},
PageLoadError: null,
GatherContext: context,
};
}
core/gather/driver/environment.js
/**
* Computes the benchmark index to get a rough estimate of device class.
* @param {LH.Gatherer.Driver['executionContext']} executionContext
* @return {Promise<number>}
*/
async function getBenchmarkIndex(executionContext) {
const status = {msg: 'Benchmarking machine', id: 'lh:gather:getBenchmarkIndex'};
log.time(status);
const indexVal = await executionContext.evaluate(pageFunctions.computeBenchmarkIndex, {
args: [],
});
log.timeEnd(status);
return indexVal;
}
function computeBenchmarkIndex() {
/**
* The GC-heavy benchmark that creates a string of length 10000 in a loop.
* The returned index is the number of times per second the string can be created divided by 10.
* The division by 10 is to keep similar magnitudes to an earlier version of BenchmarkIndex that
* used a string length of 100000 instead of 10000.
*/
function benchmarkIndexGC() {
const start = Date.now();
let iterations = 0;
while (Date.now() - start < 500) {
let s = '';
for (let j = 0; j < 10000; j++) s += 'a';
if (s.length === 1) throw new Error('will never happen, but prevents compiler optimizations');
iterations++;
}
const durationInSeconds = (Date.now() - start) / 1000;
return Math.round(iterations / 10 / durationInSeconds);
}
/**
* The non-GC-dependent benchmark that copies integers back and forth between two arrays of length 100000.
* The returned index is the number of times per second a copy can be made, divided by 10.
* The division by 10 is to keep similar magnitudes to the GC-dependent version.
*/
function benchmarkIndexNoGC() {
const arrA = [];
const arrB = [];
for (let i = 0; i < 100000; i++) arrA[i] = arrB[i] = i;
const start = Date.now();
let iterations = 0;
// Some Intel CPUs have a performance cliff due to unlucky JCC instruction alignment.
// Two possible fixes: call Date.now less often, or manually unroll the inner loop a bit.
// We'll call Date.now less and only check the duration on every 10th iteration for simplicity.
// See https://bugs.chromium.org/p/v8/issues/detail?id=10954#c1.
while (iterations % 10 !== 0 || Date.now() - start < 500) {
const src = iterations % 2 === 0 ? arrA : arrB;
const tgt = iterations % 2 === 0 ? arrB : arrA;
for (let j = 0; j < src.length; j++) tgt[j] = src[j];
iterations++;
}
const durationInSeconds = (Date.now() - start) / 1000;
return Math.round(iterations / 10 / durationInSeconds);
}
// The final BenchmarkIndex is a simple average of the two components.
return (benchmarkIndexGC() + benchmarkIndexNoGC()) / 2;
}
ベンチマーク掛けて性能を見ている?
得られた base-artifacts からエミュレーション設定等をセットアップ
page.emulate() 相当
/**
* Prepares a target to be analyzed in navigation mode by enabling protocol domains, emulation, and new document
* handlers for global APIs or error handling.
*
* This method should be used in combination with `prepareTargetForIndividualNavigation` before a specific navigation occurs.
*
* @param {LH.Gatherer.Driver} driver
* @param {LH.Config.Settings} settings
* @param {LH.NavigationRequestor} requestor
* @return {Promise<{warnings: Array<LH.IcuMessage>}>}
*/
async function prepareTargetForNavigationMode(driver, settings, requestor) {
const status = {msg: 'Preparing target for navigation mode', id: 'lh:prepare:navigationMode'};
log.time(status);
/** @type {Array<LH.IcuMessage>} */
const warnings = [];
await prepareDeviceEmulation(driver, settings);
// Automatically handle any JavaScript dialogs to prevent a hung renderer.
await dismissJavaScriptDialogs(driver.defaultSession);
// Inject our snippet to cache important web platform APIs before they're (possibly) ponyfilled by the page.
await driver.executionContext.cacheNativesOnNewDocument();
// Wrap requestIdleCallback so pages under simulation receive the correct rIC deadlines.
if (settings.throttlingMethod === 'simulate') {
await shimRequestIdleCallbackOnNewDocument(driver, settings);
}
await warmUpIntlSegmenter(driver);
const shouldResetStorage =
!settings.disableStorageReset &&
// Without prior knowledge of the destination, we cannot know which URL to clear storage for.
typeof requestor === 'string';
if (shouldResetStorage) {
const {warnings: storageWarnings} = await resetStorageForUrl(driver.defaultSession,
requestor,
settings.clearStorageTypes);
warnings.push(...storageWarnings);
}
await prepareThrottlingAndNetwork(driver.defaultSession, settings);
log.timeEnd(status);
return {warnings};
}
ダイアログを開いたら自動で閉じる処理が入っている。
/**
* Dismiss JavaScript dialogs (alert, confirm, prompt), providing a
* generic promptText in case the dialog is a prompt.
* @param {LH.Gatherer.ProtocolSession} session
* @return {Promise<void>}
*/
async function dismissJavaScriptDialogs(session) {
session.on('Page.javascriptDialogOpening', data => {
log.warn('Driver', `${data.type} dialog opened by the page automatically suppressed.`);
session
.sendCommand('Page.handleJavaScriptDialog', {
accept: true,
promptText: 'Lighthouse prompt response',
})
.catch(err => log.warn('Driver', err));
});
await session.sendCommand('Page.enable');
}
/**
* Cache native functions/objects inside window so we are sure polyfills do not overwrite the
* native implementations when the page loads.
* @return {Promise<void>}
*/
async cacheNativesOnNewDocument() {
await this.evaluateOnNewDocument(
() => {
/* c8 ignore start */
window.__nativePromise = window.Promise;
window.__nativeURL = window.URL;
window.__nativePerformance = window.performance;
window.__nativeFetch = window.fetch;
window.__ElementMatches = window.Element.prototype.matches;
window.__HTMLElementBoundingClientRect =
window.HTMLElement.prototype.getBoundingClientRect;
/* c8 ignore stop */
},
{ args: [] }
);
}
ネイティブオブジェクトを退避しておく
/**
* RequestIdleCallback shim that calculates the remaining deadline time in order to avoid a potential lighthouse
* penalty for tests run with simulated throttling. Reduces the deadline time to (50 - safetyAllowance) / cpuSlowdownMultiplier to
* ensure a long task is very unlikely if using the API correctly.
* @param {number} cpuSlowdownMultiplier
*/
function wrapRequestIdleCallback(cpuSlowdownMultiplier) {
const safetyAllowanceMs = 10;
const maxExecutionTimeMs = Math.floor((50 - safetyAllowanceMs) / cpuSlowdownMultiplier);
const nativeRequestIdleCallback = window.requestIdleCallback;
window.requestIdleCallback = (cb, options) => {
/**
* @type {Parameters<typeof window['requestIdleCallback']>[0]}
*/
const cbWrap = (deadline) => {
const start = Date.now();
// @ts-expect-error - save original on non-standard property.
deadline.__timeRemaining = deadline.timeRemaining;
deadline.timeRemaining = () => {
// @ts-expect-error - access non-standard property.
const timeRemaining = deadline.__timeRemaining();
return Math.min(timeRemaining, Math.max(0, maxExecutionTimeMs - (Date.now() - start))
);
};
deadline.timeRemaining.toString = () => {
return 'function timeRemaining() { [native code] }';
};
cb(deadline);
};
return nativeRequestIdleCallback(cbWrap, options);
};
window.requestIdleCallback.toString = () => {
return 'function requestIdleCallback() { [native code] }';
};
}
Copilot に explain させた
関数は、requestIdleCallback API をラップして、Lighthouse のテストでシミュレートされたスロットリング(CPUの遅延)によるペナルティを回避するための関数です。この関数は、requestIdleCallback のコールバックが実行される際の残り時間を調整し、長いタスクが発生しないようにします。
Long Task にならないようにしてるのかな。
/**
* Reset the storage and warn if any stored data could be affecting the scores.
* @param {LH.Gatherer.ProtocolSession} session
* @param {string} url
* @param {LH.Config.Settings['clearStorageTypes']} clearStorageTypes
* @return {Promise<{warnings: Array<LH.IcuMessage>}>}
*/
async function resetStorageForUrl(session, url, clearStorageTypes) {
/** @type {Array<LH.IcuMessage>} */
const warnings = [];
const clearDataWarnings = await storage.clearDataForOrigin(session, url, clearStorageTypes);
warnings.push(...clearDataWarnings);
const clearCacheWarnings = await storage.clearBrowserCaches(session);
warnings.push(...clearCacheWarnings);
const importantStorageWarning = await storage.getImportantStorageWarning(session, url);
if (importantStorageWarning) warnings.push(importantStorageWarning);
return {warnings};
}
ストレージデータを消す
/**
* @param {LH.Gatherer.ProtocolSession} session
* @param {string} url
* @param {LH.Config.Settings['clearStorageTypes']} clearStorageTypes
* @return {Promise<LH.IcuMessage[]>}
*/
async function clearDataForOrigin(session, url, clearStorageTypes) {
const status = {msg: 'Cleaning origin data', id: 'lh:storage:clearDataForOrigin'};
log.time(status);
const warnings = [];
const origin = new URL(url).origin;
const typesToClear = clearStorageTypes.join(',');
// `Storage.clearDataForOrigin` is one of our PROTOCOL_TIMEOUT culprits and this command is also
// run in the context of PAGE_HUNG to cleanup. We'll keep the timeout low and just warn if it fails.
session.setNextProtocolTimeout(5000);
try {
await session.sendCommand('Storage.clearDataForOrigin', {
origin: origin,
storageTypes: typesToClear,
});
} catch (err) {
if (/** @type {LH.LighthouseError} */ (err).code === 'PROTOCOL_TIMEOUT') {
log.warn('Driver', 'clearDataForOrigin timed out');
warnings.push(str_(UIStrings.warningOriginDataTimeout));
} else {
throw err;
}
} finally {
log.timeEnd(status);
}
return warnings;
}