如何從 0 到 1 搭建性能檢測系統(tǒng)
前言
前端頁面性能對用戶留存、用戶直觀體驗有著重要影響,當頁面加載時間超過 2 秒后,加載時間每增加一秒,就會有大量的用戶流失,所以做好頁面性能優(yōu)化,無疑對網(wǎng)站來說是一個非常重要的步驟。
那如何才能知道一個頁面的性能情況呢?知道了頁面性能情況后又如何進行優(yōu)化呢?一個頁面的性能指標非常多,面對一大堆性能指標,可能一個老手也一時間不知道從何開始分析。而且不同團隊,負責(zé)的業(yè)務(wù)不同,性能分析的指標也不能夠一概而論。打個比方說,對于一般的電商網(wǎng)站,一定會有很多圖片,那圖片加載的性能提升對網(wǎng)站的性能提升作用就比較大。而對于一些由表單組成的中臺頁面,提升圖片加載速度的收益遠小于電商網(wǎng)站。
總結(jié)來說,不同的團隊有著各自不同的業(yè)務(wù),業(yè)務(wù)之間千差萬別,性能指標也不能一概而論,所以用一套統(tǒng)一的檢測模型覆蓋所有場景是不現(xiàn)實的。本文將介紹如何定制一個屬于自己團隊的性能檢測平臺。
先看下政采云的性能檢測平臺——百策
在聊性能指標之前,先講一下 Lighthouse。
Lighthouse
Lighthouse 是一個開源的自動化工具,用于分析和改善 Web 應(yīng)用的質(zhì)量。運行 Lighthouse 共有 4 種方式,分別在 Chrome 開發(fā)者工具,Chrome 擴展程序,Node CLI 和 Node module。百策主要基于 Node module 方式,在其基礎(chǔ)上進行擴展開發(fā),Lighthouse 詳細使用參見 Git:https://github.com/GoogleChrome/lighthouse
下圖為 Lighthouse 檢測頁面性能的一個最終結(jié)果,可以看到其實指標已經(jīng)比較完善了。
可能有人會問,為什么不直接使用 Lighthouse。首先,由于不可描述的原因,國內(nèi)直接使用 Chrome 開發(fā)者工具中的 Lighthouse 時,會一直處于 Lighthouse is warming up 狀態(tài)。其次,Chrome 擴展程序?qū)τ谛枰卿浀捻撁嬉膊恢С帧W詈螅瑢τ谇把灾校骋恍┒ㄖ菩枨?Lighthouse 也不能全然滿足,所以要基于 Lighthouse 進行定制,做一個滿足業(yè)務(wù)要求的性能檢測平臺。
整體設(shè)計架構(gòu)
下圖是百策系統(tǒng)的一個整體架構(gòu)
-
前端主要使用的是 Antd 和 Antd Charts,包含常規(guī)頁面的展示和部分性能走勢圖表的展示。 -
服務(wù)端基于 nestjs 開發(fā),接入 Sentry 做報警監(jiān)控。helmet 用于保護系統(tǒng)免受一些眾所周知的 Web 漏洞影響。 -
node-schedule 用于每周定時計算已統(tǒng)計入系統(tǒng)的頁面性能,并通過 nodemailer 發(fā)送郵件。 -
Compression 主要用于啟用 gzip。 -
最主要的檢測服務(wù)基于 Puppeteer 和 Lighthouse 開發(fā)。
百策采集頁面性能數(shù)據(jù)的流程
百策系統(tǒng)監(jiān)控頁面的方式主要采用的方式是合成監(jiān)控,對于什么是合成監(jiān)控,可以參考此文章:螞蟻金服如何把前端性能監(jiān)控做到極致 (https://www.infoq.cn/article/Dxa8aM44oz*Lukk5Ufhy)。總結(jié)來說,合成監(jiān)控的優(yōu)勢就是:能夠采集的數(shù)據(jù)更豐富,并且可以根據(jù)不同的場景定制不同的運行環(huán)境等。首先百策要根據(jù)不同的場景,比如政采云前臺頁面、政采云中臺頁面制定不同的檢測模型。其次百策的主要目標是提升頁面性能,并且需要保證環(huán)境和硬件條件一致的情況下對頁面做性能比對,所以選擇采用合成監(jiān)控更加適合。
先看下 Chrome Lighthouse 的架構(gòu)圖(圖來源于 Lighthouse Git),主要基于 4 個主要步驟實現(xiàn),分別是交互驅(qū)動,收集,審計以及記錄組成,參考了 Chrome Lighthouse,百策的檢測模型邏輯也主要由這 4 步組成:
1、頁面交互后,發(fā)起請求調(diào)用服務(wù)。
2、遍歷當前頁面所需要的收集器,合并為一個總的收集器,并采集數(shù)據(jù)。
3、將第二步采集到的數(shù)據(jù)做性能計算和評分。
4、將性能檢測結(jié)果存入數(shù)據(jù)庫。
百策采集頁面性能數(shù)據(jù)的實現(xiàn)方案
百策實現(xiàn)頁面性能數(shù)據(jù)采集的方案主要依靠無頭瀏覽器 Puppeteer 結(jié)合 Lighthouse,Puppeteer 是 Chrome 團隊提供的一個無界面 Chrome 工具,人稱無頭瀏覽器,通過 API 來控制 Node 端的 Chrome。百策的主要邏輯是在服務(wù)端起一個無需顯示的 Chrome,通過 Lighthouse 的 API 新建一個標簽頁并打開,Lighthouse 會計算具體的性能指標,具體的檢測邏輯可以參考下圖。接下來我會用關(guān)鍵代碼說明如何實現(xiàn)其中的關(guān)鍵步驟。
○ 開始入口
以下是百策價值 1 個億的代碼,主要流程如下,鉤子函數(shù)是用于在頁面打開的不同時間獲取性能數(shù)據(jù)
/**
* 執(zhí)行頁面信息收集
*
* @param {PassContext} passContext
*/
async run(runOptions: RunOptions) {
const gathererResults = {};
// 使用 Puppeteer 創(chuàng)建無頭瀏覽器,創(chuàng)建頁面
const passContext = await this.prepare(runOptions);
try {
// 根據(jù)用戶是否輸入了用戶名和密碼判斷是否要登錄政采云
await this.preLogin(passContext);
// 頁面打開前的鉤子函數(shù)
await this.beforePass(passContext);
// 打開頁面,獲取頁面數(shù)據(jù)
await this.getLhr(passContext);
// 頁面打開后的鉤子函數(shù)
await this.afterPass(passContext, gathererResults);
// 收集頁面性能
return await this.collectArtifact(passContext, gathererResults);
} catch (error) {
throw error;
} finally {
// 關(guān)閉頁面和無頭瀏覽器
await this.disposeDriver(passContext);
}
}
○ 創(chuàng)建無頭瀏覽器
創(chuàng)建無頭瀏覽器和頁面,并指定瀏覽器對應(yīng)的寬高,指定運行的參數(shù),關(guān)于瀏覽器的參數(shù)可以參考如下文章:Puppeteer API (https://zhaoqize.github.io/puppeteer-api-zh_CN/#?product=Puppeteer&version=v5.3.0&show=api-puppeteerlaunchoptions)。可以將 headless 設(shè)置為 false 看到瀏覽器的創(chuàng)建和 page 的新建,本地調(diào)試可以使用。
/**
* 登錄前準備工作,創(chuàng)建瀏覽器和頁面
*
* @param {RunOptions} runOptions
*/
async prepare(runOptions: RunOptions) {
// puppeteer 啟動的配置項
const launchOptions: puppeteer.LaunchOptions = {
headless: true, // 是否無頭模式
defaultViewport: { width: 1440, height: 960 }, // 指定打開頁面的寬高
// 瀏覽器實例的參數(shù)配置,具體配置可以參考此鏈接:https://peter.sh/experiments/chromium-command-line-switches/
args: ['--no-sandbox', '--disable-dev-shm-usage'],
executablePath: '/usr/bin/chromium-browser', // 默認 Chromium 執(zhí)行的路徑,此路徑指的是服務(wù)器上 Chromium 安裝的位置
};
// 服務(wù)器上運行時使用服務(wù)器上獨立安裝的 Chromium
// 本地運行的時候使用 node_modules 中的 Chromium
if (process.env.NODE_ENV === 'development') {
delete launchOptions.executablePath;
}
// 創(chuàng)建瀏覽器對象
const browser = await puppeteer.launch(launchOptions);
// 獲取瀏覽器對象的默認第一個標簽頁
const page = (await browser.pages())[0];
// 返回瀏覽器和頁面對象
return { browser, page };
}
○ 模擬登錄
模擬登錄的場景可以參考另一篇,自動化 Web 性能分析之 Puppeteer 爬蟲實踐中的第四節(jié),大致的實現(xiàn)邏輯如下:通過無頭瀏覽器打開政采云登錄頁,通過 Puppeteer API 模擬輸入用戶名密碼,并模擬點擊登錄按鈕。根據(jù)同一瀏覽器下相同的域名共享 Cookie 的特性,再新開標簽頁打開需要檢測的 URL,便可以開始性能檢測。
○ 打開頁面
如何在 Puppeteer 中使用 Lighthouse 可以參考 Using Puppeteer with Lighthouse (https://github.com/GoogleChrome/lighthouse/blob/master/docs/puppeteer.md)。下面的代碼主要檢測的是桌面端 Web 頁面的性能,后續(xù)會放開更改檢測環(huán)境的功能:可以根據(jù)政采云域名來判斷頁面是手機端還是電腦端,根據(jù)不同的系統(tǒng)環(huán)境,切換不同的瀏覽器參數(shù)。
/**
* 在 Puppeteer 中使用 Lighthouse
*
* @param {RunOptions} runOptions
*/
async getLhr(passContext: PassContext) {
// 獲取瀏覽器對象和檢測鏈接
const { browser, url } = passContext;
// 開始檢測
const { artifacts, lhr } = await lighthouse(url, {
port: new URL(browser.wsEndpoint()).port,
output: 'json',
logLevel: 'info',
emulatedFormFactor: 'desktop',
throttling: {
rttMs: 40,
throughputKbps: 10 * 1024,
cpuSlowdownMultiplier: 1,
requestLatencyMs: 0, // 0 means unset
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
disableDeviceEmulation: true,
onlyCategories: ['performance'], // 是否只檢測 performance
// chromeFlags: ['--disable-mobile-emulation', '--disable-storage-reset'],
});
// 回填數(shù)據(jù)
passContext.lhr = lhr;
passContext.artifacts = artifacts;
}
○ 鉤子函數(shù)
鉤子函數(shù)實際是一個抽象類,在運行不同的 Gathering 時,對應(yīng)的 Class 會實現(xiàn)該抽象類。鉤子函數(shù)的主要功能在于不同時期注冊回調(diào),主要有 2 個鉤子函數(shù),beforePass 和 afterPass。beforePass 的作用主要是在頁面還沒加載前先注冊一些監(jiān)聽器,比如說想在頁面 load 之后,就拿到 DOM 節(jié)點的深度,那就需要在 beforePass 中注冊監(jiān)聽。afterPass 主要是頁面性能統(tǒng)計完成之后,返回結(jié)構(gòu)化的數(shù)據(jù)。
/**
* 執(zhí)行所有收集器中的 afterPass 方法
*
* @param {PassContext} passContext
* @param {GathererResults} gathererResults
*/
async afterPass(passContext: PassContext, gathererResults: GathererResults) {
const { page, gatherers } = passContext;
// 遍歷所有收集器,執(zhí)行 afterPass 方法
for (const gatherer of gatherers) {
const gathererResult = await gatherer.afterPass(passContext);
gathererResults[gatherer.name] = gathererResult;
}
// 執(zhí)行完所有方法后截圖記錄
gathererResults.screenshotBuffer = await page.screenshot();
}
○ 收集器的實現(xiàn)
百策總共有 6 個收集器,分別是 Domstats Gathering,Image Elements Gathering,Lighthouse Gathering,Metrics Gathering, Network Recorder Gathering 和 Performance Gathering。
每個收集器都會實現(xiàn)特定的收集功能:
-
Domstats Gathering:收集 DOM 相關(guān)的數(shù)據(jù),比如 DOM 元素數(shù)量,DOM 最大深度,document 是否有滾動條等。 -
Image Elements Gathering:收集所有的圖片,并記錄下圖片的寬高,定位等屬性。 -
Lighthouse Gathering:收集 Lighthouse 相關(guān)的指標:比如 FCP、LCP、TBT、CLS 等等。 -
Metrics Gathering:收集 JS 事件監(jiān)聽數(shù)量,JS 堆棧大小等。 -
Network Recorder Gathering:收集所有頁面請求,包括狀態(tài)碼,請求方式,請求頭,響應(yīng)頭等。 -
Performance Gathering:主要記錄了 window.performance 下的一些數(shù)據(jù),用于計算一些時間。
以 Domstats Gathering 做為例子,詳細說明如何獲取頁面檢測數(shù)據(jù)。首先實現(xiàn)抽象類的 2 個方法:beforePass 和 afterPass。beforePass 的實現(xiàn)邏輯是對 page 對象添加 domcontentloaded 時間點的監(jiān)聽方法,監(jiān)聽方法的主要功能是判斷 document 是否有橫向滾動條。afterPass 方法主要是獲取 Lighthouse lhr 中的數(shù)據(jù),分析并得到 DOM 最大深度,DOM 節(jié)點數(shù)等。
import { Gatherer } from './gatherer';
import { PassContext } from '../interfaces/pass-context.interface';
// 實現(xiàn) Gatherer 抽象類
export default class DOMStats extends Gatherer {
horizontalScrollBar;
/**
* 頁面打開前的鉤子函數(shù)
*
* @param {PassContext} passContext
*/
async beforePass(passContext: PassContext) {
const { browser } = passContext;
// 當瀏覽器的對象發(fā)生變化的時候,說明新打開頁面了,此時可以獲取到標簽頁 page 對象
browser.on('targetchanged', async target => {
const page = await target.page();
// 等待 dom 文檔加載完成的時候
page.on('domcontentloaded', async () => {
// 通過 evaluate 方法可以獲取到頁面上的元素和方法
this.horizontalScrollBar = await page.evaluate(() => {
return document.body.scrollWidth > document.body.clientWidth;
});
});
});
}
/**
* 頁面執(zhí)行結(jié)束后的鉤子函數(shù)
*
* @param {PassContext} passContext
*/
async afterPass(passContext: PassContext) {
const { artifacts } = passContext;
// 從 lighthouse 結(jié)果對象 lhr 中獲取 dom 節(jié)點的 depth,width 和 totalBodyElements
const {
DOMStats: { depth, width, totalBodyElements },
} = artifacts;
return {
numElements: totalBodyElements,
maxDepth: depth.max,
maxWidth: width.max,
hasHorizontalScrollBar: !!this.horizontalScrollBar,
};
}
}
等待所有 Gathering 都執(zhí)行完成之后,數(shù)據(jù)就可以落庫了。
○ 根據(jù)模型計算得分
數(shù)據(jù)入庫后還要根據(jù)不同的模型計算不同的得分。前臺頁面重展示,并且圖片加載會比較多,中臺頁面重表單提交,所以不同的模型一定有不同的計算邏輯。在政采云,前臺頁面我們使用的框架是 Vue, 中臺頁面使用的是 React(部分頁面由于歷史原因用的還是 jQuery)。所以大致可以根據(jù)框架來區(qū)分模型。判斷框架是 Vue 還是 React 可以根據(jù) DOM 是否包含 _reactRootContainer 和 __vue__ 來判斷。
/**
* 計算得分方法,根據(jù)模型上的得分配置項最終生成得分并入庫
*
* @param {Artifact} artifact
* @param {string[]} whitelist
*/
async calc(artifact: Artifact, whitelist?: string[]): Promise
{
// 根據(jù)每條 metaid 動態(tài)加載不同的計算方法文件,每個 metaid 指的就是一個性能評分指標,比如說是否有橫向滾動條
const audit =
await
import(
`../audits/${this.meta.id}`).then(
m => m.default);
// 執(zhí)行每個計算方法文件中的 audit 方法,計算得分,比如沒有橫向滾動條的時候得5分,有橫向滾動條不得分
const { rawValue, score, displayValue, details = [] } = audit.audit(artifact, whitelist);
const auditDto =
new AuditDto();
auditDto.id =
this.meta.id;
// 檢測指標名稱展示
auditDto.title =
this.meta.title;
// 檢測指標描述
auditDto.description =
this.meta.description;
// 檢測指標詳情
auditDto.details = details;
// 檢測指標登記,判斷是否計算入得分
auditDto.level =
this.level;
// 扣分上限根據(jù)不同的 meta,可能上限也有不同,upperLimitScore 指的是扣分上限,從數(shù)據(jù)庫獲取
auditDto.score = score *
this.weight <= -
this.upperLimitScore ? -
this.upperLimitScore : score *
this.weight;
// 得分情況
auditDto.rawValue = rawValue;
// 得分如何展示
auditDto.displayValue = displayValue;
return auditDto;
}
以下是政采云前臺模型,每一項都是一個檢測指標,告警項只做提示,不實際扣分,前臺主要以圖片加載和展示為準,所以模型設(shè)計上,會更加側(cè)重頁面加載時間的關(guān)鍵指標,并且會著重考慮圖片的展示。
前面內(nèi)容主要介紹了百策的數(shù)據(jù)采集和評分功能,這也是百策最主要的功能。除了核心功能外,百策還有數(shù)據(jù)看板、提供性能解決方案、性能走勢,性能對比,定時監(jiān)測等功能。在這篇文章中我也不一一闡述了。
○ 自動檢測
當然除了上面這些手動檢測以外,百策也支持自動檢測。自動檢測的主要目的是統(tǒng)計所有收錄在系統(tǒng)中的頁面,統(tǒng)計哪些頁面性能優(yōu)化的最好,哪些優(yōu)化欠佳。具體的邏輯:每周五 2 點會對所有收錄在百策中的頁面進行檢測,將檢測成績最高的 10 個頁面,檢測成績最低的 10 個頁面,檢測成績進步最快的 10 個頁面,自動檢測的邏輯主要通過 node-schedule 實現(xiàn)。發(fā)送郵件可以 ejs 實現(xiàn)渲染模版,定義好模版后通過 nodemailer 發(fā)送即可。
import {
Injectable,
OnModuleInit,
} from '@nestjs/common';
import * as schedule from 'node-schedule';
@Injectable()
export class ScheduleService implements OnModuleInit {
onModuleInit() {
this.init();
}
async init() {
// 本地啟動時不執(zhí)行一系列定時任務(wù)
if (process.env.NODE_ENV !== 'development') {
// 每周五02:00開始收集頁面性能
schedule.scheduleJob(`hawkeye-weekly-report`, '0 0 2 * * 5', async () => {
// 調(diào)用檢測接口記錄性能評分
await this.report();
});
// 每周五18:00發(fā)送周報
schedule.scheduleJob(`hawkeye-weekly-send`, '0 0 18 * * 5', async () => {
// 發(fā)送郵件的具體實現(xiàn)方法,主要通過 ejs 渲染模版,通過 nodemailer 發(fā)送郵件
await this.send();
});
}
}
}
○ 對接魯班
關(guān)于魯班是什么,可以參考這篇文章:前端工程實踐之可視化搭建系統(tǒng),用一句話來總結(jié),可以說魯班就是政采云的頁面搭建系統(tǒng)。
在對接魯班時,主要包括了魯班頁面的性能數(shù)據(jù)的錄入和魯班頁面的錄入(方便后續(xù)每周定時檢測)。
-
魯班性能數(shù)據(jù)的錄入:和在魯班生成頁面時提供一個檢測按鈕,調(diào)用百策性能評分接口,生成檢測數(shù)據(jù)。 -
魯班頁面的錄入:在魯班的新頁面上線的時候,會自動調(diào)用百策錄入接口,新增的頁面會被錄入到百策系統(tǒng)中。
結(jié)尾
如果你也想搭建一個屬于自己的性能檢測平臺,并且恰巧看到了這篇文章,希望此文對你有所幫助。
本文最主要講的是如何搭建一個性能平臺。當你已經(jīng)能夠搭建性能平臺之后,不妨可以思考下業(yè)務(wù)頁面的檢測模型。
·END·
匯聚精彩的免費實戰(zhàn)教程
喜歡本文,點個“在看”告訴我
