1

声明

本来想实时做一个示例讲述Lighthouse的用法,然后,本地安装了lighthousev8.1.0,悲剧了,使用的v7.5.0的配置不兼容。

遂,整理一下自己使用v7.5.0的输出,最后补充一点点v8.1.0的使用,真的只有一点点...因为没执行下去。

Lighthouse使用说明

这里不是具体的场景,只是介绍Lighthouse的用法,相信,您会找到自己的应用场景的。

前端性能监控模型

Lighthouse主要是用于前端性能监控,两种常见模型:合成监控、真实用户监控。

合成监控(Synthetic Monitoring,SYN)

合成监控,就是在一个模拟场景里,去提交一个需要做性能检测的页面,通过一系列的工具、规则去运行你的页面,提取一些性能指标,得出一个性能/审计报告

真实用户监控(Real User Monitoring,RUM)

真实用户监控,就是用户在我们的页面上访问,访问之后就会产生各种各样的性能数据,我们在用户离开页面的时候,把这些性能数据上传到我们的日志服务器上,进行数据的提取清洗加工,最后在我们的监控平台上进行展示的一个过程
对比项合成监控SYN真实用户监控RUM
实现难度及成本较低较高
采集数据丰富度丰富基础
数据样本量较小大(视业务体量)
适合场景支持团队自有业务,对性能做定性分析,或配合CI做小数据量的监控分析作为中台产品支持前台业务,对性能做定量分析,结合业务数据进行深度挖掘

Lighthouse是什么

Lighthouse 是一个开源的自动化工具,用于分析和改善 Web 应用的质量。

Lighthouse使用环境

  • Chrome开发者工具
  • 安装扩展程序
  • Node CLI
  • Node module

Lighthouse组成部分

  • 驱动Driver

    通过Chrome Debugging Protocol和Chrome进行交互。
  • 收集器Gatherer

    决定在页面加载过程中采集哪些信息,将采集的信息输出为Artifact
  • 审查器Audit

    将 Artifact 作为输入,审查器会对其运行 1 个测试,然后分配通过/失败/得分的结果。
  • 报告Reporter

    将审查的结果分组到面向用户的报告中(如最佳实践)。对该部分加权求和然后得出评分。

Lighthouse工作流程

简单来说Lighthouse的工作流程就是:建立连接 -> 收集日志 -> 分析 -> 生成报告。

Lighthousev7.5.0 NPM包使用示例

初始化开发环境

mkdir lh && cd $_ // 在命令行中创建项目目录、进入目录
npm init -y // 初始化Node环境
npm i -S puppeteer // 提供浏览器环境,其它NPM包也可以
npm i -S lighthouse // 安装lighthouse
//         ^  lighthouse@7.5.0

初始化运行

const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { URL } = require('url');
(async() => {
    const url = 'https://huaban.com/discovery/';
    const browser = await puppeteer.launch({
      headless: false, // 调试时设为 false,可显式的运行Chrome
      defaultViewport: null,
    });
    const lhr = await lighthouse(
      url,
      {
        port: (new URL(browser.wsEndpoint())).port,
        output: 'json',
        logLevel: 'info',
      }
    );
    console.log(lhr)
    await browser.close();
})();

以花瓣为例,puppeteer目前仅提供浏览器环境,lighthouse通过browser.wsEndpoint()与浏览器进行通信。

生成报告

通过初始化运行,我们能看到lighthouse的执行,却无法直观的看到结果。

const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { URL } = require('url');
const path = require('path');
const printer = require('lighthouse/lighthouse-cli/printer');
const Reporter = require('lighthouse/lighthouse-core/report/report-generator');
function generateReport(runnerResult) {
  const now = new Date();
  const Y = now.getFullYear();
  const M = now.getMonth();
  const D = now.getDate();
  const H = now.getHours();
  const m = now.getMinutes();
  const filename = `lhr-report@${Y}-${M + 1 < 10 ? '0' + (M + 1) : M + 1}-${D}-${H}-${m}.html`;
  const htmlReportPath = path.join(__dirname, 'public', filename);
  const reportHtml = Reporter.generateReport(runnerResult.lhr, 'html');
  printer.write(reportHtml, 'html', htmlReportPath);
}
(async() => {
    const url = 'https://huaban.com/discovery/';
    const browser = await puppeteer.launch({
      headless: false,
      defaultViewport: null,
    });
    const lhr = await lighthouse(
      url,
      {
        port: (new URL(browser.wsEndpoint())).port,
        output: 'json',
        logLevel: 'info',
      }
    );
    generateReport(lhr)
    await browser.close();
})();

手动创建一个public目录(这里不通过代码检测是否有目录了),新增generateReport方法,将报告结果输出为html

将输出的HTML用浏览器打开,可以直观地看到输出结果。
默认展示

默认有五个分类:
五个默认分类

国际化语言设置

(async() => {
    const url = 'https://huaban.com/discovery/';
    const browser = await puppeteer.launch({
      headless: false,
      defaultViewport: null,
    });
    const lhr = await lighthouse(
      url,
      {
        port: (new URL(browser.wsEndpoint())).port,
        output: 'json',
        logLevel: 'info',
      },
      {
        settings: {
          locale: 'zh' //  国际化
        }
      }
    );
    generateReport(lhr)
    await browser.close();
})();

这里,我们可以看到lighthouse(url, flags, configJSON)接收三个参数。

自定义收集器Gatherer

父级Gatherer —— 继承目标

// https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/gather/gatherers/gatherer.js
class Gatherer {
  /**
   * @return {keyof LH.GathererArtifacts}
   */
  get name() {
    // @ts-expect-error - assume that class name has been added to LH.GathererArtifacts.
    return this.constructor.name;
  }

  /* eslint-disable no-unused-vars */

  /**
   * Called before navigation to target url.
   * @param {LH.Gatherer.PassContext} passContext
   * @return {LH.Gatherer.PhaseResult}
   */
  beforePass(passContext) { }

  /**
   * Called after target page is loaded. If a trace is enabled for this pass,
   * the trace is still being recorded.
   * @param {LH.Gatherer.PassContext} passContext
   * @return {LH.Gatherer.PhaseResult}
   */
  pass(passContext) { }

  /**
   * Called after target page is loaded, all gatherer `pass` methods have been
   * executed, and — if generated in this pass — the trace is ended. The trace
   * and record of network activity are provided in `loadData`.
   * @param {LH.Gatherer.PassContext} passContext
   * @param {LH.Gatherer.LoadData} loadData
   * @return {LH.Gatherer.PhaseResult}
   */
  afterPass(passContext, loadData) { }

  /* eslint-enable no-unused-vars */
}

自定义示例

import { Gatherer } from 'lighthouse';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DevtoolsLog = require('lighthouse/lighthouse-core/gather/devtools-log.js');

class LIMGGather extends Gatherer {
  /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
  meta = {
    supportedModes: [ 'navigation' ],
    dependencies: { DevtoolsLog: DevtoolsLog.symbol },
  };

  /**
   * @param {LH.Artifacts.NetworkRequest[]} networkRecords
   */
  indexNetworkRecords(networkRecords) {
    return networkRecords.reduce((arr, record) => {
      // An image response in newer formats is sometimes incorrectly marked as "application/octet-stream",
      // so respect the extension too.
      const isImage = /^image/.test(record.mimeType) || /\.(avif|webp)$/i.test(record.url);
      // The network record is only valid for size information if it finished with a successful status
      // code that indicates a complete image response.
      if (isImage) {
        arr.push(record);
      }

      return arr;
    }, []);
  }

  /**
   * @param {LH.Gatherer.FRTransitionalContext} context
   * @param {LH.Artifacts.NetworkRequest[]} networkRecords
   * @return {Promise<LH.Artifacts['ImageElements']>}
   */
  async _getArtifact(_context, networkRecords) {
    const imageNetworkRecords = this.indexNetworkRecords(networkRecords);

    return imageNetworkRecords;
  }

  /**
   * @param {LH.Gatherer.PassContext} passContext
   * @param {LH.Gatherer.LoadData} loadData
   * @return {Promise<LH.Artifacts['ImageElements']>}
   */
  async afterPass(passContext, loadData) {
    return this._getArtifact({ ...passContext, dependencies: {} }, loadData.networkRecords);
  }
}

module.exports = LIMGGather;

单独的Gatherer是没有任何用途的,只是收集Audit需要用到的中间数据,最终展示的数据为Audit的输出。

所以,我们讲述完自定义Audit后,再说明Gatherer如何使用。

Notes:收集器Gatherer必须有个name属性,默认是父类get name()返回的this.constructor.name;,如需自定义,重写get name()方法。

Notes: Gatherer最好不要和默认收集器重名,若configJSON.configPath配置的等同默认路径,会忽略自定义 —— 后续会详细讲述。

自定义审计Audit

父级Audit —— 继承目标

Audit类有很多方法,个人用到的主要重写的也就metaaudit

// https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/audits/audit.js
class Audit {
  ...
  /**
   * @return {LH.Audit.Meta}
   */
  static get meta() {
    throw new Error('Audit meta information must be overridden.');
  }
  ...
  /**
   * @return {Object}
   */
  static get defaultOptions() {
    return {};
  }
  ...
  /* eslint-disable no-unused-vars */

  /**
   *
   * @param {LH.Artifacts} artifacts
   * @param {LH.Audit.Context} context
   * @return {LH.Audit.Product|Promise<LH.Audit.Product>}
   */
  static audit(artifacts, context) {
    throw new Error('audit() method must be overriden');
  }
  ...
}

自定义示例

import { Audit } from 'lighthouse';
import { Pages, AuditStrings, Scores } from '../../config/metrics';
import { toPrecision } from '../../util';
class LargeImageOptimizationAudit extends Audit {
  static get meta() {
    return {
      ...Pages[AuditStrings.limg],
      // ^ 重要的是id字段,定义分类时需要指定
      scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
      // ^ 分数的展示模式,只有Audit.SCORING_MODES.NUMERIC才有分值,若取其它值,分值score始终为0.
      requiredArtifacts: [ 'LIMGGather' ],
      // ^ 定义依赖的Gatherers
    };
  }
  static audit(artifacts) {
    const limitSize = 50;
    const images = artifacts.LIMGGather;
    // ^ 解构或直接访问获取Gatherer收集的数据。
    /**
    * 后续的逻辑是,判断大于50K的图片数量。
    * 以当前指标总分 - 大于50K图片数量 * 出现一次扣step分
    * 计算最终分数
    */
    const largeImages = images.filter(img => img.resourceSize > limitSize * 1024);
    const scroeConfig = Scores[AuditStrings.limg];
    const score = scroeConfig.scores - largeImages.length * scroeConfig.step;
    const finalScore = toPrecision((score < 0 ? 0 : score) / scroeConfig.scores);
    //      ^ Lighthouse中Audit、categories中的分数都是0~1范围内的

    const headings = [
      { key: 'url', itemType: 'thumbnail', text: '资源预览' },
      { key: 'url', itemType: 'url', text: '图片资源地址' },
      { key: 'resourceSize', itemType: 'bytes', text: '原始大小' },
      { key: 'transferSize', itemType: 'bytes', text: '传输大小' },
      /**
      * key:返回对象中details字段Audit.makeTableDetails方法第二个参数中对应的键值
      * itemType是Lighthouse识别对象key对应值,来使用不同的样式展示的。
      * itemType类型文档https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/report/html/renderer/details-renderer.js#L266
      */
      // const headings = [
      //   { key: 'securityOrigin', itemType: 'url', text: '域' },
      //   { key: 'dnsStart', itemType: 'ms', granularity: '0.001', text: '起始时间' },
      //   { key: 'dnsEnd', itemType: 'ms', granularity: '0.001', text: '结束时间' },
      //   { key: 'dnsConsume', itemType: 'ms', granularity: '0.001', text: '耗时' },
      // ];
      /////itemType === 'ms'时,可以设置精度granularity
    ];
    return {
      score: finalScore,
      displayValue: `${largeImages.length} / ${images.length} Size >${limitSize}KB`,
      // ^ 当前审计项的总展示
      details: Audit.makeTableDetails(headings, largeImages),
      // ^ 当前审计项的细节展示
      // ^ table类型的展示,要求第二个参数largeImages是平铺的对象元素构成的数组。
      // https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/report/html/renderer/details-renderer.js
    };
  }
}
module.exports = LargeImageOptimizationAudit;

这里,我们仍然不讲述Gatherer、Audit如何使用,我们继续讲述Categories时,统一讲述如何使用。

自定义分类Categories

import { LargeImageOptimizationAudit } from '../gathers';
//          ^ gathers文件夹下定义了统一入口index.js,导出所有的自定义收集器
import { LIMGGather } from '../audits';
//        ^ audits文件夹下定义了统一入口index.js,导出所有的自定义审查器
module.exports = {
  extends: 'lighthouse:default', // 决定是否包含默认audits,就是上述默认五个分类
  passes: [{
    passName: 'defaultPass',
    gatherers: [
      LIMGGather , // 自定义Gather的应用 
    ],
  }],
  audits: [
    LargeImageOptimizationAudit , // 自定义Audit的应用 
  ],
  categories: {
    mysite: {
      title: '自定义指标',
      description: '我的自定义指标',
      auditRefs: [
        {
          id: 'large-images-optimization', 
          // ^ Audit.id,自定义Audit.meta时指定;
          // 给自定义Audit单独定义一个分类。
          weight: 1
          // ^ 当前审计项所占的比重,权重weight总和为100!
        },
      ],
    },
  },
};

来一个非上述配置的自定义审计、分类的展示效果(因为这篇文章是后续整理的)。
自定义审计displayValue
以上是自定义审计返回对象中定义的displayValue字段。
自定义审计details
以上是自定义审计返回对象中details: Audit.makeTableDetails(headings, largeImages),展示示例。
最终的展示效果:
最终效果
这里的展示效果,是去除默认五个分类后的展示,通过注释掉上述配置的extends: 'lighthouse:default',即可去掉。

Notes:去掉extends: 'lighthouse:default',后,若使用内置或自定义的Audit依赖requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],lighthouse运行会报错:

errorMessage: "必需的 traces 收集器未运行。"
或
errorMessage: "Required traces gatherer did not run."

需要补充以下配置:

passes = [
  {
    passName: 'defaultPass',
    recordTrace: true,// 开启Traces收集器
    gatherers: [
    ],
  },
];

优化

NPM包与Chrome Devtools中的Lighthouse分值差异大

NPM包使用Lighthouse进行合成监控,模拟页面在较慢的连接上加载,会限制 DNS 解析的往返行程以及建立 TCP 和 SSL 连接。
而Chrome Devtools的Lighthouse做了很多优化,也没有进行节流限制,即使设置节流,也只是接收服务器响应时的延迟,而不是模拟每次往返双向。

NPM包使用时,添加参数--throttling-method=devtools来平衡差异。

Lighthouse切换桌面模式

Lighthouse默认是移动端模式,不同版本配置不同,配置需要对应版本。

// https://github.com/GoogleChrome/lighthouse/discussions/12058
// 以下是configJSON.settings的配置
// eslint-disable-next-line @typescript-eslint/no-var-requires
const constants = require('lighthouse/lighthouse-core/config/constants');
...
  getSettings() {
    return (function(isDesktop) {
      if (isDesktop) {
        return {
          // extends: 'lighthouse:default', // 是否包含默认audits
          locale: 'zh',
          formFactor: 'desktop', // 必须的lighthouse模拟Device
          screenEmulation: { // 结合formFactor使用,要匹配
            ...constants.screenEmulationMetrics.desktop,
          },
          emulatedUserAgent: constants.userAgents.desktop, // 结合formFactor使用,要匹配
        };
      }
      return {
        // extends: 'lighthouse:default', // 是否包含默认audits
        locale: 'zh',
        formFactor: 'mobile', // 必须的lighthouse模拟Device
        screenEmulation: { // 结合formFactor使用,要匹配
          ...constants.screenEmulationMetrics.mobile,
        },
        emulatedUserAgent: constants.userAgents.mobile, // 结合formFactor使用,要匹配
      };
    })(this.isDesktop);
  }

Lighthouse的Gather、Audits路径配置

对象配置

见上述使用;

路径配置

// 引入方式:相对于项目根目录,设置相对路径
...
      lightHouseConfig: {
        // onlyCategories: onlyCategories.split(','), // https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md
        reportPath: path.join(__dirname, '../public/'), // 测试报告存储目录
        passes: [
          {
            passName: 'defaultPass',
            gatherers: [
              'app/gathers/large-images-optimization.ts',
            ],
          },
        ],
        audits: [
          'app/audits/large-images-optimization.ts',
        ],
        categories: {    
          mysite: {
            title: '自定义指标',
            description: '我的自定义指标',
            auditRefs: [
              {
                id: 'large-images-optimization', 
                weight: 1
              },
            ],
        },
...

其中:源代码使用require(path)的形式调用,而不是require(path).default,只支持module.epxorts = class Module导出

BasePath路径配置

理论上配置:

  // lighthouse运行的第二个参数flags
  {
    port: new URL(this.browser.wsEndpoint()).port,
    configPath: path.join(__filename), // 查找gather、audit的基准
                            //    ^路径定位到文件,源码内会上溯到目录结构;若定位到目录,源码内会上溯到上一级目录
  },
  // lighthouse运行的第三个参数configJson
  ...
    passes: [
      {
        passName: 'defaultPass',
        gatherers: [
          'large-images-optimization',
        ],
      },
    ],
    audits: [
      'large-images-optimization',
    ],
...

然而,"lighthouse": "^7.5.0",源码中:

requirePath = resolveModulePath(gathererPath, configDir, 'gatherer'); // 检索gather
const absolutePath = resolveModulePath(auditPath, configDir, 'audit'); // 检索audit

第三个参数并没有使用。
即v7.5.0会在配置的configPath下查找对应的'large-images-optimization'。

现做如下妥协:

// lighthouse第三个参数configJson
  ...
    passes: [
      {
        passName: 'defaultPass',
        gatherers: [
          'gathers/large-images-optimization',
        ],
      },
    ],
    audits: [
      'audits/large-images-optimization',
    ],
...

Gather、Audti配置引入源码实现

function expandGathererShorthand(gatherer) {
  if (typeof gatherer === 'string') {
    // just 'path/to/gatherer'
    return {path: gatherer};
  } else if ('implementation' in gatherer || 'instance' in gatherer) {
    // {implementation: GathererConstructor, ...} or {instance: GathererInstance, ...}
    return gatherer;
  } else if ('path' in gatherer) {
    // {path: 'path/to/gatherer', ...}
    if (typeof gatherer.path !== 'string') {
      throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
    }
    return gatherer;
  } else if (typeof gatherer === 'function') {
    // just GathererConstructor
    return {implementation: gatherer};
  } else if (gatherer && typeof gatherer.beforePass === 'function') {
    // just GathererInstance
    return {instance: gatherer};
  } else {
    throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
  }
}
//https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/config-helpers.js#L342

function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
  const gathererDefn = expandGathererShorthand(gathererJson);
  if (gathererDefn.instance) {
    return {
      instance: gathererDefn.instance,
      implementation: gathererDefn.implementation,
      path: gathererDefn.path,
    };
  } else if (gathererDefn.implementation) {
    const GathererClass = gathererDefn.implementation;
    return {
      instance: new GathererClass(),
      implementation: gathererDefn.implementation,
      path: gathererDefn.path,
    };
  } else if (gathererDefn.path) {
    const path = gathererDefn.path;
    return requireGatherer(path, coreGathererList, configDir);
  } else {
    throw new Error('Invalid expanded Gatherer: ' + JSON.stringify(gathererDefn));
  }
}

补充:Gather、Audit重名问题

默认GathererAudit的检索只是lighthouse-core包中对应目录的检索,若自定义的GatherAudit,且使用时使用字符串标识,一定不能和lighthouse-core中的重名。

重名的话,优先使用lighthouse-core中的gatheraudit

function requireGatherer(gathererPath, coreGathererList, configDir) {
  const coreGatherer = coreGathererList.find(a => a === `${gathererPath}.js`);

  let requirePath = `../gather/gatherers/${gathererPath}`;
  if (!coreGatherer) {
    // Otherwise, attempt to find it elsewhere. This throws if not found.
    requirePath = resolveModulePath(gathererPath, configDir, 'gatherer');
  }

  const GathererClass = /** @type {GathererConstructor} */ (require(requirePath));

  return {
    instance: new GathererClass(),
    implementation: GathererClass,
    path: gathererPath,
  };
}

不重名的话,会有一个检索顺序

  1. 相对路径检索;
  2. process.cwd()同级目录下检索;
  3. 配置flags.configPath的话,该路径下查找;

    1. flags.configPath只是查找gathereraudits资源的基准,config的设置和该配置无关。
    function resolveModulePath(moduleIdentifier, configDir, category) {
      try {
     return require.resolve(moduleIdentifier);
      } catch (e) {}
    
      try {
     return require.resolve(moduleIdentifier, {paths: [process.cwd()]});
      } catch (e) {}
    
      const cwdPath = path.resolve(process.cwd(), moduleIdentifier);
      try {
     return require.resolve(cwdPath);
      } catch (e) {}
    
      const errorString = 'Unable to locate ' + (category ? `${category}: ` : '') +
     `\`${moduleIdentifier}\`.
      Tried to require() from these locations:
        ${__dirname}
        ${cwdPath}`;
    
      if (!configDir) {
     throw new Error(errorString);
      }
      const relativePath = path.resolve(configDir, moduleIdentifier);
      try {
     return require.resolve(relativePath);
      } catch (requireError) {}
      try {
     return require.resolve(moduleIdentifier, {paths: [configDir]});
      } catch (requireError) {}
    
      throw new Error(errorString + `
        ${relativePath}`);
    }

报告解析

我们看到报告中有部分是已通过,这部分怎么解析的呢?

附上源码片段:

// node_modules/lighthouse/lighthouse-core/report/html/renderer/category-renderer.js
...
// 报告模版 解析
 render(category, groupDefinitions = {}) {
    const element = this.dom.createElement('div', 'lh-category');
    this.createPermalinkSpan(element, category.id);
    element.appendChild(this.renderCategoryHeader(category, groupDefinitions));

    // Top level clumps for audits, in order they will appear in the report.
    /** @type {Map<TopLevelClumpId, Array<LH.ReportResult.AuditRef>>} */
    const clumps = new Map();
    clumps.set('failed', []);
    clumps.set('warning', []);
    clumps.set('manual', []);
    clumps.set('passed', []);
    clumps.set('notApplicable', []);

    // Sort audits into clumps.
    for (const auditRef of category.auditRefs) {
      const clumpId = this._getClumpIdForAuditRef(auditRef);
      const clump = /** @type {Array<LH.ReportResult.AuditRef>} */ (clumps.get(clumpId)); // already defined
      clump.push(auditRef);
      clumps.set(clumpId, clump);
    }

    // Render each clump.
    for (const [clumpId, auditRefs] of clumps) {
      if (auditRefs.length === 0) continue;

      if (clumpId === 'failed') {
        const clumpElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
        clumpElem.classList.add(`lh-clump--failed`);
        element.appendChild(clumpElem);
        continue;
      }

      const description = clumpId === 'manual' ? category.manualDescription : undefined;
      const clumpElem = this.renderClump(clumpId, {auditRefs, description});
      element.appendChild(clumpElem);
    }

    return element;
  }
...
  _getClumpIdForAuditRef(auditRef) {
    const scoreDisplayMode = auditRef.result.scoreDisplayMode;
    if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
      return scoreDisplayMode;
    }

    if (Util.showAsPassed(auditRef.result)) {
      if (this._auditHasWarning(auditRef)) {
        return 'warning';
      } else {
        return 'passed';
      }
    } else {
      return 'failed';
    }
  }
...
// node_modules/lighthouse/lighthouse-core/report/html/renderer/util.js
...
const ELLIPSIS = '\u2026';
const NBSP = '\xa0';
const PASS_THRESHOLD = 0.9;
const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,';

const RATINGS = {
  PASS: {label: 'pass', minScore: PASS_THRESHOLD},
  AVERAGE: {label: 'average', minScore: 0.5},
  FAIL: {label: 'fail'},
  ERROR: {label: 'error'},
};
...
  static showAsPassed(audit) {
    switch (audit.scoreDisplayMode) {
      case 'manual':
      case 'notApplicable':
        return true;
      case 'error':
      case 'informative':
        return false;
      case 'numeric':
      case 'binary':
      default:
        return Number(audit.score) >= RATINGS.PASS.minScore; // 当audit分值大于0.9时,报告展示通过;
    }
  }
...

补充:收集器中依赖浏览器

如这里需要知道是否支持webp格式

async function supportWebp(context) {
  const { driver } = context;
  const expression = function() {
    const elem = document.createElement('canvas');
    // eslint-disable-next-line no-extra-boolean-cast
    if (!!(elem.getContext && elem.getContext('2d'))) {
      // was able or not to get WebP representation
      return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
    }
    // very old browser like IE 8, canvas not supported
    return false;
  };
  return await driver.executionContext.evaluate(
  //                                      ^ 返回Promise
    expression,
    /**
    * expression函数声明,不能是字符串,evaluateSync需要是字符串
    * 你可能看到过evaluateSync,这是lighthousev7.0.0及其以前版本支持的API。
    */
    {
      args: [], // {required} args,否则报错
    },
  );
}
...

完整示例:

import { Gatherer } from 'lighthouse';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DevtoolsLog = require('lighthouse/lighthouse-core/gather/devtools-log.js');

async function supportWebp(context) {
  const { driver } = context;
  const expression = function() {
    const elem = document.createElement('canvas');
    // eslint-disable-next-line no-extra-boolean-cast
    if (!!(elem.getContext && elem.getContext('2d'))) {
      // was able or not to get WebP representation
      return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
    }
    // very old browser like IE 8, canvas not supported
    return false;
  };
  return await driver.executionContext.evaluate(
    expression,
    {
      args: [],
    },
  );
}
class WebpImageGather extends Gatherer {
  /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
  meta = {
    supportedModes: [ 'navigation' ],
    dependencies: { DevtoolsLog: DevtoolsLog.symbol },
  };

  /**
   * @param {LH.Artifacts.NetworkRequest[]} networkRecords
   */
  indexNetworkRecords(networkRecords) {
    return networkRecords.reduce((arr, record) => {
      const isImage = /^image/.test(record.mimeType) || /\.(avif|webp)$/i.test(record.url);
      if (isImage) {
        arr.push(record);
      }

      return arr;
    }, []);
  }

  /**
   * @param {LH.Gatherer.FRTransitionalContext} _context
   * @param {LH.Artifacts.NetworkRequest[]} networkRecords
   * @return {Promise<LH.Artifacts['ImageElements']>}
   */
  async _getArtifact(context, networkRecords) {
    const isSupportWebp = await supportWebp(context);
    const imagesNetworkRecords = this.indexNetworkRecords(networkRecords);

    return {
      isSupportWebp,
      imagesNetworkRecords,
    };
  }
  async afterPass(context, loadData) {
    return this._getArtifact(context, loadData.networkRecords);
  }
}

module.exports = WebpImageGather;

也可参考源码中 ImageElements的声明及pageFunctions的定义。

补充:Lighouthousev.8.1.0

对比v.7.5.0

report-generator引用地址

const Reporter = require('lighthouse/report/report-generator');
//                            ^ 该引入地址,是v8.1.0更新的,老版引入地址为'lighthouse/lighthouse-core/report/report-generator'

lighthouse执行

若使用第三个参数,则locale为必填项。

    const lhr = await lighthouse(
      url,
      {
        port: (new URL(browser.wsEndpoint())).port,
        output: 'json',
        logLevel: 'info',
      }
    );

locale配置

lighthousev8.1.0,若nodev12.0.0及其以前的版本,需要手动安装full-icu并在命令行中添加参数 node --icu-data-dir="./node_modules/full-icu" index.js
然后,在配置项中才能定义:

const lhr = await lighthouse(
      url,
      {
        port: (new URL(browser.wsEndpoint())).port,
        output: 'json',
        logLevel: 'info',
      },
      {
        settings: {
          extends: 'lighthouse:default', // 发现不再是默认配置的定义了
          locale: 'zh',
        },
        passes: [
          {
            passName: 'defaultPass',
          }
        ],
        audits: [

        ],
      }
    );

参考文档

Lighthouse 网易云音乐实践技术文档

Lighthouse政采云实践技术文档

Puppeteer政采云实践技术文档

Chromium浏览器实例参数配置

Lighthouse Driver源码官方实现

Lighthouse Drive模块与浏览器双向通信协议JSON

Devtools-Protocol协议官方文档

Puppeteer5.3.0中文文档

Puppeteer官方文档

Lighthouse源码


米花儿团儿
1.3k 声望75 粉丝