这是第 74 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注咱们吧~ 本文首发于政采云前端博客: 如何从 0 到 1 搭建性能检测系统
前端页面性能对用户留存、用户直观体验有着重要影响,当页面加载时间超过 2 秒后,加载时间每增长一秒,就会有大量的用户流失,因此作好页面性能优化,无疑对网站来讲是一个很是重要的步骤。前端
那如何才能知道一个页面的性能状况呢?知道了页面性能状况后又如何进行优化呢?一个页面的性能指标很是多,面对一大堆性能指标,可能一个老手也一时间不知道从何开始分析。并且不一样团队,负责的业务不一样,性能分析的指标也不可以一律而论。打个比方说,对于通常的电商网站,必定会有不少图片,那图片加载的性能提高对网站的性能提高做用就比较大。而对于一些由表单组成的中台页面,提高图片加载速度的收益远小于电商网站。vue
总结来讲,不一样的团队有着各自不一样的业务,业务之间千差万别,性能指标也不能一律而论,因此用一套统一的检测模型覆盖全部场景是不现实的。本文将介绍如何定制一个属于本身团队的性能检测平台。node
先看下政采云的性能检测平台——百策react
在聊性能指标以前,先讲一下 Lighthouse。git
Lighthouse 是一个开源的自动化工具,用于分析和改善 Web 应用的质量。运行 Lighthouse 共有 4 种方式,分别在 Chrome 开发者工具,Chrome 扩展程序,Node CLI 和 Node module。百策主要基于 Node module 方式,在其基础上进行扩展开发,Lighthouse 详细使用参见 Git:https://github.com/GoogleChrome/lighthousegithub
下图为 Lighthouse 检测页面性能的一个最终结果,能够看到其实指标已经比较完善了。vue-router
可能有人会问,为何不直接使用 Lighthouse。首先,因为不可描述的缘由,国内直接使用 Chrome 开发者工具中的 Lighthouse 时,会一直处于 Lighthouse is warming up 状态。其次,Chrome 扩展程序对于须要登陆的页面也不支持。最后,对于前言中,某一些定制需求 Lighthouse 也不能全然知足,因此要基于 Lighthouse 进行定制,作一个知足业务要求的性能检测平台。chrome
下图是百策系统的一个总体架构typescript
百策系统监控页面的方式主要采用的方式是合成监控,对于什么是合成监控,能够参考此文章:蚂蚁金服如何把前端性能监控作到极致。总结来讲,合成监控的优点就是:可以采集的数据更丰富,而且能够根据不一样的场景定制不一样的运行环境等。首先百策要根据不一样的场景,好比政采云前台页面、政采云中台页面制定不一样的检测模型。其次百策的主要目标是提高页面性能,而且须要保证环境和硬件条件一致的状况下对页面作性能比对,因此选择采用合成监控更加适合。数据库
先看下 Chrome Lighthouse 的架构图(图来源于 Lighthouse Git),主要基于 4 个主要步骤实现,分别是交互驱动,收集,审计以及记录组成,参考了 Chrome Lighthouse,百策的检测模型逻辑也主要由这 4 步组成:
一、页面交互后,发起请求调用服务。
二、遍历当前页面所须要的收集器,合并为一个总的收集器,并采集数据。
三、将第二步采集到的数据作性能计算和评分。
四、将性能检测结果存入数据库。
百策实现页面性能数据采集的方案主要依靠无头浏览器 Puppeteer 结合 Lighthouse,Puppeteer 是 Chrome 团队提供的一个无界面 Chrome 工具,人称无头浏览器,经过 API 来控制 Node 端的 Chrome。百策的主要逻辑是在服务端起一个无需显示的 Chrome,经过 Lighthouse 的 API 新建一个标签页并打开,Lighthouse 会计算具体的性能指标,具体的检测逻辑能够参考下图。接下来我会用关键代码说明如何实现其中的关键步骤。
如下是百策价值 1 个亿的代码,主要流程以下,钩子函数是用于在页面打开的不一样时间获取性能数据
/** * 执行页面信息收集 * * @param {PassContext} passContext */ async run(runOptions: RunOptions) { const gathererResults = {}; // 使用 Puppeteer 建立无头浏览器,建立页面 const passContext = await this.prepare(runOptions); try { // 根据用户是否输入了用户名和密码判断是否要登陆政采云 await this.preLogin(passContext); // 页面打开前的钩子函数 await this.beforePass(passContext); // 打开页面,获取页面数据 await this.getLhr(passContext); // 页面打开后的钩子函数 await this.afterPass(passContext, gathererResults); // 收集页面性能 return await this.collectArtifact(passContext, gathererResults); } catch (error) { throw error; } finally { // 关闭页面和无头浏览器 await this.disposeDriver(passContext); } }
建立无头浏览器和页面,并指定浏览器对应的宽高,指定运行的参数,关于浏览器的参数能够参考以下文章:Puppeteer API。能够将 headless 设置为 false 看到浏览器的建立和 page 的新建,本地调试可使用。
/** * 登陆前准备工做,建立浏览器和页面 * * @param {RunOptions} runOptions */ async prepare(runOptions: RunOptions) { // puppeteer 启动的配置项 const launchOptions: puppeteer.LaunchOptions = { headless: true, // 是否无头模式 defaultViewport: { width: 1440, height: 960 }, // 指定打开页面的宽高 // 浏览器实例的参数配置,具体配置能够参考此连接:https://peter.sh/experiments/chromium-command-line-switches/ args: ['--no-sandbox', '--disable-dev-shm-usage'], executablePath: '/usr/bin/chromium-browser', // 默认 Chromium 执行的路径,此路径指的是服务器上 Chromium 安装的位置 }; // 服务器上运行时使用服务器上独立安装的 Chromium // 本地运行的时候使用 node_modules 中的 Chromium if (process.env.NODE_ENV === 'development') { delete launchOptions.executablePath; } // 建立浏览器对象 const browser = await puppeteer.launch(launchOptions); // 获取浏览器对象的默认第一个标签页 const page = (await browser.pages())[0]; // 返回浏览器和页面对象 return { browser, page }; }
模拟登陆的场景能够参考另外一篇,“百策系统”实现模拟登陆的实现,大体的实现逻辑以下:经过无头浏览器打开政采云登陆页,经过 Puppeteer API 模拟输入用户名密码,并模拟点击登陆按钮。根据同一浏览器下相同的域名共享 Cookie 的特性,再新开标签页打开须要检测的 URL,即可以开始性能检测。
如何在 Puppeteer 中使用 Lighthouse 能够参考Using Puppeteer with Lighthouse。下面的代码主要检测的是桌面端 Web 页面的性能,后续会放开更改检测环境的功能:能够根据政采云域名来判断页面是手机端仍是电脑端,根据不一样的系统环境,切换不一样的浏览器参数。
/** * 在 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'], }); // 回填数据 passContext.lhr = lhr; passContext.artifacts = artifacts; }
钩子函数实际是一个抽象类,在运行不一样的 Gathering 时,对应的 Class 会实现该抽象类。钩子函数的主要功能在于不一样时期注册回调,主要有 2 个钩子函数,beforePass 和 afterPass。beforePass 的做用主要是在页面还没加载前先注册一些监听器,好比说想在页面 load 以后,就拿到 DOM 节点的深度,那就须要在 beforePass 中注册监听。afterPass 主要是页面性能统计完成以后,返回结构化的数据。
/** * 执行全部收集器中的 afterPass 方法 * * @param {PassContext} passContext * @param {GathererResults} gathererResults */ async afterPass(passContext: PassContext, gathererResults: GathererResults) { const { page, gatherers } = passContext; // 遍历全部收集器,执行 afterPass 方法 for (const gatherer of gatherers) { const gathererResult = await gatherer.afterPass(passContext); gathererResults[gatherer.name] = gathererResult; } // 执行完全部方法后截图记录 gathererResults.screenshotBuffer = await page.screenshot(); }
百策总共有 6 个收集器,分别是 Domstats Gathering,Image Elements Gathering,Lighthouse Gathering,Metrics Gathering, Network Recorder Gathering 和 Performance Gathering。
每一个收集器都会实现特定的收集功能:
以 Domstats Gathering 作为例子,详细说明如何获取页面检测数据。首先实现抽象类的 2 个方法:beforePass 和 afterPass。beforePass 的实现逻辑是对 page 对象添加 domcontentloaded 时间点的监听方法,监听方法的主要功能是判断 document 是否有横向滚动条。afterPass 方法主要是获取 Lighthouse lhr 中的数据,分析并获得 DOM 最大深度,DOM 节点数等。
import { Gatherer } from './gatherer'; import { PassContext } from '../interfaces/pass-context.interface'; // 实现 Gatherer 抽象类 export default class DOMStats extends Gatherer { horizontalScrollBar; /** * 页面打开前的钩子函数 * * @param {PassContext} passContext */ async beforePass(passContext: PassContext) { const { browser } = passContext; // 当浏览器的对象发生变化的时候,说明新打开页面了,此时能够获取到标签页 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; }); }); }); } /** * 页面执行结束后的钩子函数 * * @param {PassContext} passContext */ async afterPass(passContext: PassContext) { const { artifacts } = passContext; // 从 lighthouse 结果对象 lhr 中获取 dom 节点的 depth,width 和 totalBodyElements const { DOMStats: { depth, width, totalBodyElements }, } = artifacts; return { numElements: totalBodyElements, maxDepth: depth.max, maxWidth: width.max, hasHorizontalScrollBar: !!this.horizontalScrollBar, }; } }
等待全部 Gathering 都执行完成以后,数据就能够落库了。
数据入库后还要根据不一样的模型计算不一样的得分。前台页面重展现,而且图片加载会比较多,中台页面重表单提交,因此不一样的模型必定有不一样的计算逻辑。在政采云,前台页面咱们使用的框架是 Vue, 中台页面使用的是 React(部分页面因为历史缘由用的仍是 jQuery)。因此大体能够根据框架来区分模型。判断框架是 Vue 仍是 React 能够根据 DOM 是否包含 _reactRootContainer
和 __vue__
来判断。
/** * 计算得分方法,根据模型上的得分配置项最终生成得分并入库 * * @param {Artifact} artifact * @param {string[]} whitelist */ async calc(artifact: Artifact, whitelist?: string[]): Promise<AuditDto> { // 根据每条 metaid 动态加载不一样的计算方法文件,每一个 metaid 指的就是一个性能评分指标,好比说是否有横向滚动条 const audit = await import(`../audits/${this.meta.id}`).then(m => m.default); // 执行每一个计算方法文件中的 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; // 扣分上限根据不一样的 meta,可能上限也有不一样,upperLimitScore 指的是扣分上限,从数据库获取 auditDto.score = score * this.weight <= -this.upperLimitScore ? -this.upperLimitScore : score * this.weight; // 得分状况 auditDto.rawValue = rawValue; // 得分如何展现 auditDto.displayValue = displayValue; return auditDto; }
如下是政采云前台模型,每一项都是一个检测指标,告警项只作提示,不实际扣分,前台主要以图片加载和展现为准,因此模型设计上,会更加侧重页面加载时间的关键指标,而且会着重考虑图片的展现。
前面内容主要介绍了百策的数据采集和评分功能,这也是百策最主要的功能。除了核心功能外,百策还有数据看版、提供性能解决方案、性能走势,性能对比,定时监测等功能。在这篇文章中我也不一一阐述了。
固然除了上面这些手动检测之外,百策也支持自动检测。自动检测的主要目的是统计全部收录在系统中的页面,统计哪些页面性能优化的最好,哪些优化欠佳。具体的逻辑:每周五 2 点会对全部收录在百策中的页面进行检测,将检测成绩最高的 10 个页面,检测成绩最低的 10 个页面,检测成绩进步最快的 10 个页面,自动检测的逻辑主要经过 node-schedule 实现。发送邮件能够 ejs 实现渲染模版,定义好模版后经过 nodemailer 发送便可。
import { Injectable, OnModuleInit, } from '@nestjs/common'; import * as schedule from 'node-schedule'; @Injectable() export class ScheduleService implements OnModuleInit { onModuleInit() { this.init(); } async init() { // 本地启动时不执行一系列定时任务 if (process.env.NODE_ENV !== 'development') { // 每周五02:00开始收集页面性能 schedule.scheduleJob(`hawkeye-weekly-report`, '0 0 2 * * 5', async () => { // 调用检测接口记录性能评分 await this.report(); }); // 每周五18:00发送周报 schedule.scheduleJob(`hawkeye-weekly-send`, '0 0 18 * * 5', async () => { // 发送邮件的具体实现方法,主要经过 ejs 渲染模版,经过 nodemailer 发送邮件 await this.send(); }); } } }
关于鲁班是什么,能够参考这篇文章:前端工程实践之可视化搭建系统,用一句话来总结,能够说鲁班就是政采云的页面搭建系统。
在对接鲁班时,主要包括了鲁班页面的性能数据的录入和鲁班页面的录入(方便后续每周定时检测)。
若是你也想搭建一个属于本身的性能检测平台,而且恰巧看到了这篇文章,但愿此文对你有所帮助。
本文最主要讲的是如何搭建一个性能平台。当你已经可以搭建性能平台以后,不妨能够思考下业务页面的检测模型。
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com