探究 LightHouse 工作流程
什麼是 Lighthouse
Lighthouse analyzes web apps and web pages, collecting modern performance metrics and insights on developer best practices.
使用方式
-
Chrome 瀏覽器插件。Chrome 插件的形式提供了更加友好的用戶界面,方便讀取報告。
-
Chrome DevTools。該工具集成在最新版本的 Chrome 瀏覽器中,無需安裝即可使用。
-
Lighthouse CLI 命令行工具。方便將 Lighthouse 集成到持續集成系統中。
-
代碼中引用。我們也能通過 Node.js 模塊引入 Lighthouse 工具包。
原理結構 [1]
Gathering
Driver 驅動
通過 Chrome Debugging Protocol 和 Puppeteer[2] (提供無頭瀏覽器環境模擬頁面操作) / 進行交互。
Chrome Debugging Protocol(CDP)
Chrome DevTools 協議允許使用工具來檢測、檢查、調試和分析 Chromium、Chrome 和其他基於 Blink 的瀏覽器。 在 Chrome 擴展中,Chrome protocol 利用 chrome.debugger
Api 通過 WebSocket[3] 來建立連接。
Instrumentation 分爲多個 Domains(DOM, Debugger, Network 等)。每個 Domain 定義了許多它支持的命令和它生成的事件。命令和事件都是固定結構的序列化 JSON 對象。
CDP Domains,紅色爲實驗性
Domain 必須 enable()
後纔可以發出事件。一旦啓用 enable,它們將刷新表示狀態的所有事件。因此,網絡事件僅在 enable()
後纔會發出。所有協議代理解析 enable()
的回調。比如:
// will NOT work
driver.defaultSession.sendCommand('Security.enable').then(_ => {
driver.defaultSession.on('Security.securityStateChanged', state => { /* ... */ });
})
// WILL work! happy happy. :)
driver.defaultSession.on('Security.securityStateChanged', state => { /* ... */ }); // event binding is synchronous
driver.defaultSession.sendCommand('Security.enable');
- 調試協議:閱讀更好地調試協議 Better debugging of the Protocol[4]。
配置
passes
passes 屬性控制如何加載請求的 URL,以及在加載時收集哪些關於頁面的信息。pass 數組中的每個條目代表頁面的一次加載,
每個 pass
都定義了基本設置,例如等待頁面加載多長時間、是否記錄 trace 文件。此外,每次傳遞都定義了要使用的 gatherer
列表。gatherer
可以從頁面中讀取信息以生成 artifacts
,稍後 Audits 使用這些artifacts
提供 Lighthouse 報告。
具體的 pass 配置示例:
{
passes: [{
passName: 'fastPass',
atherers: ['fast-gatherer'],
},
{
passName: 'slowPass',
recordTrace: true,
useThrottling: true,
networkQuietThresholdMs: 5000,
gatherers: ['slow-gatherer'],
}]
}
Gatherers 採集器
決定在頁面加載過程中採集哪些信息,將採集的信息輸出爲 artifacts
。使用 Driver 採集頁面信息。用 --gather-mode
指令運行可以獲得 3 個採集產物:
-
artifacts.json
: 所有采集器的輸出。 -
defaultPass.trace.json
: 大多數性能指標。可以在 DevTools 性能面板中查看。 -
defaultPass.devtoolslog.json
: DevTools Protocol[5] 事件的日誌。
每一個 gatherer
繼承自相同的基類 Gatherer,基類 Gatherer 定義了傳遞生命週期的 n 個方法。 gatherer
的artifacts
是生命週期方法返回的最後一個未定義值,所有方法都可以直接返回artifacts
或返回解析爲該值的 Promise。子類只需實現生命週期方法即可。
比如用於 js 覆蓋率的 gatherer:
該實例實現了 startInstrumentation 、stopInstrumentation、getArtifact 3 個生命週期方法,其
class JsUsage extends FRGatherer {
meta = {
supportedModes: ['snapshot', 'timespan', 'navigation'],
};
constructor() {
super();
this._scriptUsages = [];
}
async startInstrumentation(context) {
const session = context.driver.defaultSession;
await session.sendCommand('Profiler.enable');
await session.sendCommand('Profiler.startPreciseCoverage', {detailed: false});
}
async stopInstrumentation(context) {
const session = context.driver.defaultSession;
const coverageResponse = await session.sendCommand('Profiler.takePreciseCoverage');
this._scriptUsages = coverageResponse.result;
await session.sendCommand('Profiler.stopPreciseCoverage');
await session.sendCommand('Profiler.disable');
}
async getArtifact() {
const usageByScriptId = {};
for (const scriptUsage of this._scriptUsages) {
if (scriptUsage.url === '' || scriptUsage.url === '_lighthouse-eval.js') {
continue;
}
usageByScriptId[scriptUsage.scriptId] = scriptUsage;
}
return usageByScriptId;
}
}
class FRGatherer { meta = { supportedModes : []}; //在任意時間段內開始觀察頁面 startInstrumentation ( passContext ) { } //Sensitive開始觀察頁面 startSensitiveInstrumentation ( passContext ) { } //Sensitive停止觀察頁面的方法 stopSensitiveInstrumentation ( passContext ) { } //在任意時間段內結束觀察頁面 stopInstrumentation ( passContext ) { } //收集有關頁面的結果 getArtifact ( passContext ) { } /** * Legacy */ get name () {} async beforePass ( passContext ) {} pass ( passContext ) { } async afterPass ( passContext, loadData ) {}
當 pass 中定義的所有 gatherers 運行完後,就會生成一箇中間產物 artifacts,此後 Lighthouse 就可以斷開與瀏覽器的連接,只使用 artifacts 進行後續的分析。
Trace 鏈路追蹤
core/lib/tracehouse/trace-processor.js
提供了鏈路到更有意義對象的轉換。每個原始 trace event[6] 都具有以微秒爲單位增長的時間戳、線程 ID、進程 ID、持續時間以及其他適用的元數據屬性(比如事件類型、任務名稱、幀等)
Example Trace Event
{
'pid': 41904, // process ID
'tid': 1295, // thread ID
'ts': 1676836141, // timestamp in microseconds
'ph': 'X', // trace event type
'cat': 'toplevel', // trace category from which this event came
'name': 'MessageLoop::RunTask', // relatively human-readable description of the trace event
'dur': 64, // duration of the task in microseconds
'args': {}, // contains additional data such as frame when applicable
}
Processed trace
Processed trace 可識別關鍵時刻的 trace 事件((navigation start, FCP, LCP, DCL, trace end 等),並過濾出主進程和主線程事件的視圖。
{
processEvents: [/* all trace events in the main process */],
mainThreadEvents: [/* all trace events on the main thread */],
timings: {
timeOrigin: 0, // timeOrigin is always 0
msfirstContentfulPaint: 150, // firstContentfulPaint time in ms after time origin
/* other key moments */
traceEnd: 16420, // traceEnd time in ms after time origin
},
timestamps: {
timeOrigin: 623000000, // timeOrigin timestamp in microseconds, marks the start of the navigation of interest
firstContentfulPaint: 623150000, // firstContentfulPaint timestamp in microseconds
/* other key moments */
traceEnd: 639420000, // traceEnd timestamp in microseconds
},
}
實現
-
Connecting to browser
-
Resetting state with about:blank
-
Navigate to about:blank
-
Benchmarking machine
-
Initializing…
-
Preparing target for navigation mode
-
Running defaultPass pass
-
Resetting state with about:blank
-
Navigate to about:blank
-
Preparing target for navigation
-
Cleaning origin data
-
Cleaning browser cache
-
Preparing network conditions
-
Beginning devtoolsLog and trace
-
Loading page & waiting for onload
-
Navigating to https:XXX
-
Gathering in-page: XXXXXXXX. (xN)
-
Gathering trace
-
Gathering devtoolsLog & network records
-
Gathering XXX (xN)
begin();
|
→ runLighthouse();
|
→ legacyNavigation();
async function legacyNavigation(url, flags = {}, configJSON, userConnection) {
//...
const connection = userConnection || new CriConnection(flags.port, flags.hostname);
const artifacts = await Runner.gather(() => {
const requestedUrl = UrlUtils.normalizeUrl(url);
return Runner._gatherArtifactsFromBrowser(requestedUrl, options, connection);
}, options);
return Runner.audit(artifacts, options);
}
static async _gatherArtifactsFromBrowser(requestedUrl, runnerOpts, connection) {
//創建connection的Driver
const driver = runnerOpts.driverMock || new Driver(connection);
const gatherOpts = {
driver,
requestedUrl,
settings: runnerOpts.config.settings,
computedCache: runnerOpts.computedCache,
};
const artifacts = await GatherRunner.run(runnerOpts.config.passes, gatherOpts);
return artifacts;
}
/****** GatherRunner ****/
static async run(passConfigs, options) {
//1.Connecting to browser
//通過 Websocket 建立連接, 基於 Chrome Debugging Protocol 通信
// CDPSession 實例用於與 Chrome Devtools 協議的原生通信
await driver.connect();
// 在 devtools/extension 案例中,我們在嘗試清除狀態時仍不能在站點上
// 所以我們首先導航到 about:blank,然後應用我們的仿真和設置
// 2.Resetting state with about:blank & 3.Navigating to blankPage
await GatherRunner.loadBlank(driver);
// 4. Benchmarking machine
const baseArtifacts = await GatherRunner.initializeBaseArtifacts(options);
// ...processing benchmarkIndex
// 5. Initializing…
await GatherRunner.setupDriver(driver, options);
let isFirstPass = true;
// each pass
for (const passConfig of passConfigs) {
const passContext = {
gatherMode: 'navigation',
driver,
url: options.requestedUrl,
settings: options.settings,
passConfig,
baseArtifacts,
computedCache: options.computedCache,
LighthouseRunWarnings: baseArtifacts.LighthouseRunWarnings,
};
//Starting from about:blank, load the page and run gatherers for this pass.
const passResults = await GatherRunner.runPass(passContext);
Object.assign(artifacts, passResults.artifacts);
// If we encountered a pageLoadError, don't try to keep loading the page in future passes.
if (passResults.pageLoadError && passConfig.loadFailureMode === 'fatal') {
baseArtifacts.PageLoadError = passResults.pageLoadError;
break;
}
if (isFirstPass) {
await GatherRunner.populateBaseArtifacts(passContext);
isFirstPass = false;
}
}
await GatherRunner.disposeDriver(driver, options);
return finalizeArtifacts(baseArtifacts, artifacts);
} catch (err) {
// Clean up on error. Don't await so that the root error, not a disposal error, is shown.
GatherRunner.disposeDriver(driver, options);
throw err;
}
}
_connectToSocket(response) {
const url = response.webSocketDebuggerUrl;
this._pageId = response.id;
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, {
perMessageDeflate: false,
});
ws.on('open', () => {
this._ws = ws;
resolve();
});
ws.on('message', data => this.handleRawMessage(/** @type {string} */ (data)));
ws.on('close', this.dispose.bind(this));
ws.on('error', reject);
});
}
static async setupDriver(driver, options) {
//...
await GatherRunner.assertNoSameOriginServiceWorkerClients(session, options.requestedUrl);
// 6. Preparing target for navigation mode,通過爲全局 API 或錯誤處理啓用協議域、仿真和新文檔處理程序,準備在導航模式下分析的目標。
await prepare.prepareTargetForNavigationMode(driver, options.settings);
}
static async runPass(passContext) {
//7. Running defaultPass pass
const gathererResults = {};
const {driver, passConfig} = passContext;
// 8.Resetting state with about:blank 9.Navigating to about:blankGo to about:blank
// set up
await GatherRunner.loadBlank(driver, passConfig.blankPage);
// 10.Preparing target for navigation ~ 13.Preparing network conditions
const {warnings} = await prepare.prepareTargetForIndividualNavigation(
driver.defaultSession,
passContext.settings,
{
requestor: passContext.url,
disableStorageReset: !passConfig.useThrottling,
disableThrottling: !passConfig.useThrottling,
blockedUrlPatterns: passConfig.blockedUrlPatterns,
}
);
// run `startInstrumentation() /beforePass()` on gatherers.
passContext.LighthouseRunWarnings.push(...warnings);
await GatherRunner.beforePass(passContext, gathererResults);
// 14.Beginning devtoolsLog and trace,
// await driver.beginDevtoolsLog(); await driver.beginTrace(settings);
await GatherRunner.beginRecording(passContext);
//15.Loading page & waiting for onload ,16.Navigating to https:XXX
const {navigationError: possibleNavError} = await GatherRunner.loadPage(driver, passContext);
//17.Gathering in-page: XXXXXXXX,run `pass()` on gatherers.
await GatherRunner.pass(passContext, gathererResults);
const loadData = await GatherRunner.endRecording(passContext);
//18.Gathering trace 19.Gathering devtoolsLog & network records
await emulation.clearThrottling(driver.defaultSession);
//process page error
// If no error, save devtoolsLog and trace.
GatherRunner._addLoadDataToBaseArtifacts(passContext, loadData, passConfig.passName);
// 20.Gathering XXX. Run `afterPass()(stopInstrumentation -> getArtifact )` on gatherers and return collected artifacts.
await GatherRunner.afterPass(passContext, loadData, gathererResults);
const artifacts = GatherRunner.collectArtifacts(gathererResults);
return artifacts;
}
Auditing
Audits 審查器
-
Audits 是對單個功能 / 優化 / 指標的測試,Gatherer 採集的 artifacts 作爲輸入,審查器會對其測試,然後得出相應的測評分數結果。
-
Computed Artifacts :根據 artifacts 的需求生成,有額外的含義,經常在在多個 audits 中共享。
-
測評結果結構:
配置
-
audits
-
audits 屬性控制要運行和包含在 Lighthouse 報告中的審計。 查看更多示例以瞭解如何將自定義審覈添加到您的配置中。
-
具體的 audits 配置示例:
-
{ audits: [ 'first-contentful-paint', 'byte-efficiency/uses-optimized-images', ] }
實現
-
Analyzing and running audits
-
Auditing: XXX
-
Generating results...
async function legacyNavigation(url, flags = {}, configJSON, userConnection) {
//...
return Runner.audit(artifacts, options);
}
static async audit(artifacts, options) {
//...
//1. Analyzing and running audits &2.Auditing: XXX
const auditResultsById = await Runner._runAudits(settings, config.audits, artifacts,
lighthouseRunWarnings, computedCache);
//3.Generating results...
if (artifacts.LighthouseRunWarnings) {
lighthouseRunWarnings.push(...artifacts.LighthouseRunWarnings);
}
//....
}
Report 報告
客戶端根據生成 Audit 結果的 LHR.json (Lighthouse Result) 生成結果報告頁。評分報告,它包含了性能(Performance),訪問無障礙(Accessibility),最佳實踐(Best Practice),搜索引擎優化(SEO),PWA(Progressive Web App)5 個部分,每一項下面又有若干小項(audit),還有詳細診斷結果和優化建議,幫助開發者有針對性地進行優化。
例如:在 Lighthouse 8 中,性能得分由以下幾項的得分按不同的權重相加而得:
Lighthouse 8 中性能指標權重
如何確定指標分數
以性能評分 [7] 爲例,一旦 Lighthouse 收集完性能指標(主要以毫秒爲單位報告),它會通過查看指標值在其 Lighthouse 評分分佈中的位置,將每個原始指標值轉換爲從 0 到 100 的指標分數。評分分佈是從 HTTP Archive[8] 上真實網站性能數據的性能指標得出的對數正態分佈。
FCP in HTTP Archive
Lighthouse 評分曲線模型使用 HTTPArchive 數據來確定兩個控制點,然後設置對數正態曲線的形狀。HTTPArchive 數據的第 25 個百分位數變爲 50 分(中值控制點),第 8 個百分位數變爲 90 分(良好 / 綠色控制點)。在探索下面的評分曲線圖時,請注意在 0.50 和 0.92 之間,度量值和分數之間存在近乎線性的關係。0.96 左右的分數是上面的 “收益遞減點”,曲線拉開,需要越來越多的指標改進來提高已經很高的分數。
探索 TTI 的評分曲線 [9]
指標得分和性能得分根據以下範圍進行着色:
0 至 49(紅色):差
50 至 89(橙色):需要改進
90 至 100(綠色):良好
爲了提供良好的用戶體驗,網站應該努力獲得良好的分數(90-100)。
實現
static async audit(artifacts, options) {
//....
//conclusion of the lighthouse result object
const axeVersion = artifacts.Accessibility?.version;
const credits = {
'axe-core': axeVersion,
}
let categories = {};
if (config.categories) {
categories = ReportScoring.scoreAllCategories(config.categories, auditResultsById);
}
// Replace ICU message references with localized strings; save replaced paths in lhr.
i18nLhr.i18n.icuMessagePaths = format.replaceIcuMessages(i18nLhr, settings.locale);
// LHR has now been localized.
const lhr = /** @type {LH.Result} */ (i18nLhr);
if (settings.auditMode) {
const path = Runner._getDataSavePath(settings);
assetSaver.saveLhr(lhr, path);
}
// 生成報告
const report = ReportGenerator.generateReport(lhr, settings.output);
return {lhr, artifacts, report};
}
參考資料
[1]
原理結構: https://github.com/GoogleChrome/lighthouse/blob/main/docs/architecture.md
[2]
Puppeteer: https://github.com/puppeteer/puppeteer
[3]
WebSocket: https://github.com/websockets/ws
[4]
Better debugging of the Protocol: https://github.com/GoogleChrome/lighthouse/issues/184
[5]
DevTools Protocol: https://chromedevtools.github.io/devtools-protocol/
[6]
trace event: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
[7]
性能評分: https://web.dev/performance-scoring/
[8]
HTTP Archive: https://httparchive.org/reports/state-of-the-web
[9]
探索 TTI 的評分曲線: https://www.desmos.com/calculator/o98tbeyt1t
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DUZUHataXTSX_1uvInJcnA