前言
目前哈啰前端應(yīng)用質(zhì)量監(jiān)控使用到了 Lighthouse 作為首頁性能問題定性檢查的工具,在使用過程中,我們基于 Lighthouse Plugin 對其原有能力進(jìn)行擴(kuò)展,用于檢測 web 端和小程序的性能問題,在實(shí)踐中也積累了些經(jīng)驗(yàn),希望能通過本文的分享,給大家一些幫助。
Lighthouse 簡介
Lighthouse 是一個(gè)開源的自動化檢測工具,通過內(nèi)置審計(jì)模塊,分析 web 應(yīng)用和 web 頁面的性能指標(biāo),最終給出頁面的首屏得分和最佳實(shí)踐指南。
使用方式
使用方式有四種,具體可以參考官方使用說明(github.com/GoogleChrom…) ,啟動姿勢很重要,大家選一款適合自己的。
模塊實(shí)現(xiàn)
整體架構(gòu)圖如上,模塊的實(shí)現(xiàn)細(xì)節(jié)可以參考 Lighthouse 模塊實(shí)現(xiàn)。 Lighthouse Plugin 主要會涉及到 Gatherers、Audits、Categories 這三個(gè)模塊。
檢測報(bào)告
Lighthouse 的檢測報(bào)告默認(rèn)展示 5 類信息:
- Performance(性能)
- Progressive Web App(漸進(jìn)式 web 應(yīng)用)
- Accessibility(無障礙)
- Best Practices(最佳實(shí)踐)
- SEO(SEO)
可以通過調(diào)用不同 config 來展示其中幾類。
Lighthouse Plugin 開發(fā)入門
可以參考官方文檔 ,這里給出一個(gè)簡單的示例。開發(fā)一個(gè)plugin,主要分為以下兩步:
一、新建 Lighthouse Plugin項(xiàng)目 創(chuàng)建一個(gè) npm 包,包含 plugin.js 和 package.json:
module.exports = {
// 這里可以新增自己的 audit(審計(jì)項(xiàng))
audits: [{path: 'lighthouse-plugin-example/test-audit.js'}],
// 這里可以設(shè)置自定義分類的一些信息,比如標(biāo)題、描述、引用的審計(jì)項(xiàng)等
category: {
title: 'title',
description: 'description',
auditRefs: [{id: 'test-id', weight: 1}],
},
};
package.json
{
"name": "lighthouse-plugin-example",
"main": "plugin.js",
"peerDependencies": {
"lighthouse": "^5.6.0"
},
"devDependencies": {
"lighthouse": "^5.6.0"
}
}
復(fù)制代碼
注:Lighthouse 要按 peerDependencies 方式引入,避免重復(fù)下載。
由于上例用到了新的審計(jì)項(xiàng),所以我們也需要新建這個(gè)文件
const { Audit } = require('lighthouse');
class TestAudit extends Audit {
static get meta() {
return {
// 這里定義審計(jì)項(xiàng)的一些配置,比如標(biāo)題、異常描述、依賴的artifacts等
requiredArtifacts: ['devtoolsLogs'],
};
}
static audit(artifacts) {
// lighthouse 運(yùn)行過程會經(jīng)過一系列的采集器(gatherers),
// 每個(gè)采集器都會收集自己的目標(biāo)信息,并生成中間產(chǎn)物(artifacts)
return {
score: 1,
};
}
}
module.exports = TestAudit;
復(fù)制代碼
二、使用Lighthouse Plugin
如果是命令行,直接運(yùn)行:
復(fù)制代碼
如果是 npm 方式運(yùn)行, Lighthouse 的配置需改為:
extends: 'lighthouse:default',
plugins: ['lighthouse-plugin-example'],
settings: {
// ...
},
};
復(fù)制代碼
Lighthouse Plugin 在項(xiàng)目中的實(shí)踐
先聊一聊為什么要做 哈啰前端應(yīng)用質(zhì)量監(jiān)控的規(guī)劃之一,就是統(tǒng)一使用Lighthouse 做多端的首屏性能的定性檢查。瀏覽器端是天然支持的,難點(diǎn)是小程序,無法直接在瀏覽器中直接運(yùn)行,也就無法使用 Lighthouse 進(jìn)行定性檢查。所幸,現(xiàn)在有 taro、antmove 等一眾的開源工具,可以借助這些工具將小程序轉(zhuǎn)換為運(yùn)行在瀏覽器端的應(yīng)用,如此一來,我們可以將 Lighthouse 集成到我們的工程體系中來,使用相同的工具鏈對瀏覽器端和小程序端進(jìn)行定性檢查。
不憑空的制造需求,是商業(yè)公司技術(shù)團(tuán)隊(duì)的立足之本。在整體目標(biāo)的引導(dǎo)下,我們要考慮如何來檢查小程序的性能,傳統(tǒng)意義上的Web頁面的檢查方式是否可以直接搬過來呢?答案是否定的,而對于小程序的技術(shù)實(shí)現(xiàn)原理,Lighthouse默認(rèn)那一套不能直接照搬。運(yùn)行在瀏覽器端的小程序,F(xiàn)MP、FCI、TTI等都可以做橫向?qū)Ρ?,但其結(jié)果的可靠性還要進(jìn)行推敲,除此之外,我們需要有一套符合小程序技術(shù)特性的分析方法和工具,我們需要開發(fā)對應(yīng)的Lighthouse Plugin,對應(yīng)的Gatherers、Audits和Categories。
這張截圖來自支付寶小程序的部分檢測項(xiàng),在傳統(tǒng) web 端檢測的基礎(chǔ)上,多了許多小程序特有檢測(比如 setData 頻率、數(shù)據(jù)量、API 調(diào)用次數(shù)、同/異步調(diào)用等),這些指標(biāo)目前 Lighthouse 并非天生就覆蓋到。
為小程序定制一個(gè)插件
以下以編寫一個(gè)審計(jì)支付寶小程序Native API調(diào)用次數(shù)的插件為例,簡單的總結(jié)如下:
業(yè)務(wù)代碼注入采集邏輯 + gatherer 收集采集數(shù)據(jù) + audit 消費(fèi)數(shù)據(jù)并計(jì)算結(jié)果
業(yè)務(wù)代碼注入采集邏輯
定義一個(gè)日志采集對象window.nativeCall.push 采集一次調(diào)用行為,示例代碼如下。
window.$$nativeCall = {
calls: [],
push: function (type, callInfo) {
this.calls.push({ timestamp: Date.now(), type, callInfo });
},
};
function $$myproxy(my) {
return new Proxy(my, {
get(target, key) {
const keyValue = https://www.wxapp-union.com/target[key];
// 如果是API方法,則代理原方法
if (typeof keyValue ==='function') {
return $$myProxyFn(keyValue, target, key);
}
// 否則,直接上報(bào)了一個(gè)API屬性的調(diào)用檢測點(diǎn)
window.$$nativeCall.push('api-attr-called', JSON.stringify({
key: `my.${key}`,
}));
return keyValue;
}
});
};
function $$myProxyFn(fn, that, key) {
return (function (...args) {
// 上報(bào)了一個(gè)AP方法的調(diào)用檢測點(diǎn),攜帶自定義參數(shù)
window.$$nativeCall.push('api-method-called', JSON.stringify({
key: `my.${key}`,
args,
}));
try {
const result = fn.apply(this, args);
// 上報(bào)了一個(gè)AP方法的成功調(diào)用的檢測點(diǎn),叫 api-method-success-called
window.$$nativeCall.push('api-method-success-called', JSON.stringify({
key: `my.${key}`,
args,
result: err,
}));
} catch (err) {
// 上報(bào)了一個(gè)AP方法的異常調(diào)用檢測點(diǎn),叫 api-method-error-called
window.$$nativeCall.push('api-method-error-called', JSON.stringify({
key: `my.${key}`,
args,
result: err,
}));
}
}).bind(that);
}
復(fù)制代碼
示例代碼寫的比較簡單,只是將調(diào)用信息存儲在了window.$$nativeCall中。這段代碼需要通過編譯工具鏈來插入到小程序的業(yè)務(wù)代碼中,以攔截小程序代碼中所有的Native API的調(diào)用。
自定義 audit 審計(jì)模塊
在進(jìn)行審計(jì)分析之前,我們需要一個(gè) Gatherer 來獲取window.$$nativeCall采集到的信息。
const { Gatherer } = require('lighthouse');
class CustomNativeCall extends Gatherer {
public async afterPass(passContext) {
const { driver } = passContext;
const $$nativeCall = await driver.evaluateAsync('window.$$nativeCall');
if ($$nativeCall) {
return $$nativeCall.calls;
}
return [];
}
}
module.exports = CustomNativeCall;
復(fù)制代碼
收集器寫好了。我們需要編寫 Audit 的處理邏輯,這里,我們以統(tǒng)計(jì) Native API 調(diào)用次數(shù)(api-method-called)為例:
const { Audit } = require('lighthouse');
class ApiMethodCalledAudit extends Audit {
static get meta() {
return {
// 定義好 id,這樣 plugin.js 就能找到這個(gè)審計(jì)項(xiàng)
id: 'api-method',
// 這里引入上面定義好的收集器
requiredArtifacts: ['CustomNativeCall'],
};
}
static audit(artifacts) {
const { CustomNativeCall } = artifacts;
const apiMap = {};
CustomNativeCall.forEach((log) => {
const {
// timestamp, // 時(shí)間戳在這個(gè)審計(jì)項(xiàng)中沒用到,可以不聲明
type,
callInfo,
} = log;
// 判斷type是否是我們上報(bào)的特定類型:api-method-called
if (type === 'api-method-called') {
const { key } = JSON.parse(callInfo);
// 取出api的方法名,計(jì)數(shù)
apiMap[key] = apiMap[key] || 0;
apiMap[key] += 1;
}
});
const result = Object.keys(apiMap).reduce((res, apiKey) => {
res.push({
api: apiKey,
count: apiMap[apiKey],
});
return res;
}, []);
return {
// 生成表格明細(xì)
details: Audit.makeTableDetails(ApiMethodCalledAudit.getHeadings(), result),
// 結(jié)果展示文案
displayValue: `共找到 ${result.length} 次API方法調(diào)用`,
// 評分,區(qū)間在0-1,1表示該審計(jì)項(xiàng)滿分
score: 1,
};
}
private static getHeadings() {
return [
{
itemType: 'text',
key: 'api',
text: '名稱',
},
{
itemType: 'numeric',
key: 'count',
text: '調(diào)用次數(shù)',
},
];
}
};
module.exports = ApiMethodCalledAudit;
復(fù)制代碼
最后,在我們的 plugin.js 引入 api-method-called-audit.js
module.exports = {
audits: [{path: 'lighthouse-plugin-example/api-method-called-audit.js'}],
category: {
title: '容器',
description: 'description',
// 這里注入plugin用到的audit依賴,id取自audit里的meta.id
auditRefs: [{id: 'api-method', weight: 1}],
},
};
復(fù)制代碼
至此,數(shù)據(jù)上報(bào)、采集、消費(fèi)整個(gè)流程便打通了。
小程序中的實(shí)踐總結(jié) 以下整理了當(dāng)前我們實(shí)際項(xiàng)目中定義的部分日志類型、審計(jì)模塊。
日志類型
- api-attr-called:用于 API 屬性訪問次的相關(guān)統(tǒng)計(jì)
- api-method-error-called:用于 API 方法的異常調(diào)用次數(shù)的統(tǒng)計(jì)
- api-method-called:用于 API 方法的調(diào)用次數(shù)情況統(tǒng)計(jì)
- api-method-success-called:用于 API 方法的成功調(diào)用次數(shù)、耗時(shí)的統(tǒng)計(jì)
- set-data:用于 setData 調(diào)用次數(shù)、數(shù)據(jù)大小的統(tǒng)計(jì)
- set-data-success:用于 setData 調(diào)用耗時(shí)統(tǒng)計(jì)
Audit
- api-async-same-args-called: 異步 API 方法相同入?yún)⒄{(diào)用
- api-attr-called: API 屬性調(diào)用
- api-deprecated-called: 廢棄 API 調(diào)用
- api-duplicate-called: API 被重復(fù)調(diào)用情況(連續(xù) 20 次)
- api-error-called: API 異常調(diào)用
- api-long-time-called: API 調(diào)用耗時(shí)過長(超過 1000ms)
- api-method-called: API 方法調(diào)用
- api-sync-called: API 同步方法調(diào)用情況
- page-node-used: 文檔節(jié)點(diǎn)復(fù)雜度
- set-data-called: setData 調(diào)用頻繁(20 次/秒)
- set-data-size: setData 數(shù)據(jù)量超限(超過 256kb)
Category
我們將審計(jì)項(xiàng)分為三大類:
- performance
主要包含性能相關(guān)的審計(jì)項(xiàng),比如setData調(diào)用次數(shù)、setData單次設(shè)置的數(shù)據(jù)量、頁面節(jié)點(diǎn)數(shù)等等,這些審計(jì)項(xiàng)的優(yōu)化,可以帶來相對直觀的性能提升。
- container
主要包含容器相關(guān)的審計(jì)項(xiàng),比如小程序api(異常、重復(fù)、耗時(shí)、相同入?yún)ⅲ┱{(diào)用、小程序native屬性調(diào)用等等,這些審計(jì)項(xiàng)和小程序運(yùn)行環(huán)境直接關(guān)聯(lián)。
- best-practice
主要包含最佳實(shí)踐相關(guān)的審計(jì)項(xiàng),比如圖片轉(zhuǎn)webp、禁止廢棄api調(diào)用、請求異常處理、同步轉(zhuǎn)為異步api調(diào)用等等,這些審計(jì)項(xiàng)都和官方推薦的開發(fā)方式相關(guān)。
插件使用方法
以 npm 包方式為例,我們將 Lighthouse 的配置改為:
module.exports = {
extends: 'lighthouse:default',
plugins: [
// 這里引入了容器檢測,性能和最佳實(shí)踐如果需要的話也可以在這里引入
'lighthouse-plugin-miniprogram/plugins/container'
],
passes: [{
passName: 'defaultPass',
gatherers: [
'lighthouse-plugin-miniprogram/gatherers/custom-native-call',
],
}],
settings: {
// ...
},
};
復(fù)制代碼
運(yùn)行結(jié)果
這里我們用目前已經(jīng)寫好的統(tǒng)計(jì)項(xiàng)作為演示,使用方式和上例一致的。
最后
本文從實(shí)際業(yè)務(wù)出發(fā),簡要介紹了 Lighthouse在哈啰前端應(yīng)用質(zhì)量監(jiān)控中的運(yùn)用,對于如何在小程序中應(yīng)用好Lighthouse,我們目前也還在探索階段,歡迎交流。
參考資料
- Lighthouse-developers(文章鏈接:developers.google.com/web/tools/l…
- Lighthouse 測試內(nèi)幕(文章鏈接:juejin.im/post/5dca05…
- Web 性能優(yōu)化地圖(文章鏈接:github.com/berwin/Blog…
- Chrome DevTools Protocol(文章鏈接:chromedevtools.github.io/devtools-pr…
- Lighthouse-architecture(文章鏈接:github.com/GoogleChrom…