探究 LightHouse 工作流程

什麼是 Lighthouse

Lighthouse analyzes web apps and web pages, collecting modern performance metrics and insights on developer best practices.

使用方式

原理結構 [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');

配置

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 個採集產物:

  1. artifacts.json: 所有采集器的輸出。

  2. defaultPass.trace.json: 大多數性能指標。可以在 DevTools 性能面板中查看。

  3. defaultPass.devtoolslog.json: DevTools Protocol[5] 事件的日誌。

每一個 gatherer繼承自相同的基類 Gatherer,基類 Gatherer 定義了傳遞生命週期的 n 個方法。 gathererartifacts是生命週期方法返回的最後一個未定義值,所有方法都可以直接返回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
     },
 }

實現

  1. Connecting to browser

  2. Resetting state with about:blank

  3. Navigate to about:blank

  4. Benchmarking machine

  5. Initializing…

  6. Preparing target for navigation mode

  7. Running defaultPass pass

  8. Resetting state with about:blank

  9. Navigate to about:blank

  10. Preparing target for navigation

  11. Cleaning origin data

  12. Cleaning browser cache

  13. Preparing network conditions

  14. Beginning devtoolsLog and trace

  15. Loading page & waiting for onload

  16. Navigating to https:XXX

  17. Gathering in-page: XXXXXXXX. (xN)

  18. Gathering trace

  19. Gathering devtoolsLog & network records

  20. 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 審查器

配置

實現

  1. Analyzing and running audits

  2. Auditing: XXX

  3. 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