声明
本来想实时做一个示例讲述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)
接收三个参数。
url
- 需要检测的目标地址
flags
Lighthouse
运行的配置项- 决定运行的端口、调试地址,查找Gatherer、Audit的相对地址
- 具体配置见:https://github.com/GoogleChro...
configJSON
Lighthouse
工作的配置项- 决定如何工作,收集何种信息、如何审计、分类展示...
具体配置见:
自定义收集器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
类有很多方法,个人用到的主要重写的也就meta
、audit
。
// 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
字段。
以上是自定义审计返回对象中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重名问题
默认Gatherer
、Audit
的检索只是lighthouse-core
包中对应目录的检索,若自定义的Gather
、Audit
,且使用时使用字符串标识,一定不能和lighthouse-core
中的重名。
重名的话,优先使用lighthouse-core
中的gather
、audit
;
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
只是查找gatherer
、audits
资源的基准,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
,若node
在v12.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: [
],
}
);
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。