Lighthouse使用说明

米花儿团儿 -
Lighthouse使用说明
声明

本来想实时做一个示例讲述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 CLINode moduleLighthouse组成部分

驱动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)接收三个参数。

url

需要检测的目标地址

flags

Lighthouse运行的配置项决定运行的端口、调试地址,查找Gatherer、Audit的相对地址具体配置见:https://github.com/GoogleChro...

configJSON

Lighthouse工作的配置项决定如何工作,收集何种信息、如何审计、分类展示...

具体配置见:

https://github.com/GoogleChro...https://github.com/GoogleChro...自定义收集器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,
  };
}

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

相对路径检索;process.cwd()同级目录下检索;

配置flags.configPath的话,该路径下查找;

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.0report-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源码

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

lighthousenode.js

扩展阅读

加个好友,技术交流

1628738909466805.jpg