乐趣区

关于前端:如何从-0-到-1-搭建性能检测系统

这是第 74 篇不掺水的原创,想获取更多原创好文,请搜寻公众号关注咱们吧~ 本文首发于政采云前端博客:如何从 0 到 1 搭建性能检测零碎

前言

前端页面性能对用户留存、用户直观体验有着重要影响,当页面加载工夫超过 2 秒后,加载工夫每减少一秒,就会有大量的用户散失,所以做好页面性能优化,无疑对网站来说是一个十分重要的步骤。

那如何能力晓得一个页面的性能状况呢?晓得了页面性能状况后又如何进行优化呢?一个页面的性能指标十分多,面对一大堆性能指标,可能一个新手也一时间不晓得从何开始剖析。而且不同团队,负责的业务不同,性能剖析的指标也不可能一概而论。打个比方说,对于个别的电商网站,肯定会有很多图片,那图片加载的性能晋升对网站的性能晋升作用就比拟大。而对于一些由表单组成的中台页面,晋升图片加载速度的收益远小于电商网站。

总结来说,不同的团队有着各自不同的业务,业务之间千差万别,性能指标也不能一概而论,所以用一套对立的检测模型笼罩所有场景是不事实的。本文将介绍如何定制一个属于本人团队的性能检测平台。

先看下政采云的性能检测平台——百策

在聊性能指标之前,先讲一下 Lighthouse。

Lighthouse

Lighthouse 是一个开源的自动化工具,用于剖析和改善 Web 利用的品质。运行 Lighthouse 共有 4 种形式,别离在 Chrome 开发者工具,Chrome 扩大程序,Node CLI 和 Node module。百策次要基于 Node module 形式,在其根底上进行扩大开发,Lighthouse 具体应用参见 Git:https://github.com/GoogleChrome/lighthouse

下图为 Lighthouse 检测页面性能的一个最终后果,能够看到其实指标曾经比较完善了。

可能有人会问,为什么不间接应用 Lighthouse。首先,因为不可形容的起因,国内间接应用 Chrome 开发者工具中的 Lighthouse 时,会始终处于 Lighthouse is warming up 状态。其次,Chrome 扩大程序对于须要登录的页面也不反对。最初,对于前言中,某一些定制需要 Lighthouse 也不能全然满足,所以要基于 Lighthouse 进行定制,做一个满足业务要求的性能检测平台。

整体设计架构

下图是百策零碎的一个整体架构

  • 前端次要应用的是 Antd 和 Antd Charts,蕴含惯例页面的展现和局部性能走势图表的展现。
  • 服务端基于 nestjs 开发,接入 Sentry 做报警监控。helmet 用于爱护零碎免受一些家喻户晓的 Web 破绽影响。
  • node-schedule 用于每周定时计算已统计入零碎的页面性能,并通过 nodemailer 发送邮件。
  • Compression 次要用于启用 gzip。
  • 最次要的检测服务基于 Puppeteer 和 Lighthouse 开发。

百策采集页面性能数据的流程

百策系统监控页面的形式次要采纳的形式是合成监控,对于什么是合成监控,能够参考此文章:蚂蚁金服如何把前端性能监控做到极致。总结来说,合成监控的劣势就是:可能采集的数据更丰盛,并且能够依据不同的场景定制不同的运行环境等。首先百策要依据不同的场景,比方政采云前台页面、政采云中台页面制订不同的检测模型。其次百策的次要指标是晋升页面性能,并且须要保障环境和硬件条件统一的状况下对页面做性能比对,所以抉择采纳合成监控更加适宜。

先看下 Chrome Lighthouse 的架构图(图来源于 Lighthouse Git),次要基于 4 个次要步骤实现,别离是交互驱动,收集,审计以及记录组成,参考了 Chrome Lighthouse,百策的检测模型逻辑也次要由这 4 步组成:

1、页面交互后,发动申请调用服务。

2、遍历以后页面所须要的收集器,合并为一个总的收集器,并采集数据。

3、将第二步采集到的数据做性能计算和评分。

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:收集 DOM 相干的数据,比方 DOM 元素数量,DOM 最大深度,document 是否有滚动条等。
  • Image Elements Gathering:收集所有的图片,并记录下图片的宽高,定位等属性。
  • Lighthouse Gathering:收集 Lighthouse 相干的指标:比方 FCP、LCP、TBT、CLS 等等。
  • Metrics Gathering:收集 JS 事件监听数量,JS 堆栈大小等。
  • Network Recorder Gathering:收集所有页面申请,包含状态码,申请形式,申请头,响应头等。
  • Performance Gathering:次要记录了 window.performance 下的一些数据,用于计算一些工夫。

以 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();});
    }
  }
}

○ 对接鲁班

对于鲁班是什么,能够参考这篇文章:前端工程实际之可视化搭建零碎,用一句话来总结,能够说鲁班就是政采云的页面搭建零碎。

在对接鲁班时,次要包含了鲁班页面的性能数据的录入和鲁班页面的录入(不便后续每周定时检测)。

  • 鲁班性能数据的录入:和在鲁班生成页面时提供一个检测按钮,调用百策性能评分接口,生成检测数据。
  • 鲁班页面的录入:在鲁班的新页面上线的时候,会主动调用百策录入接口,新增的页面会被录入到百策零碎中。

结尾

如果你也想搭建一个属于本人的性能检测平台,并且凑巧看到了这篇文章,心愿此文对你有所帮忙。

本文最次要讲的是如何搭建一个性能平台。当你曾经可能搭建性能平台之后,无妨能够思考下业务页面的检测模型。

举荐浏览

浅析 vue-router 源码和动静路由权限调配

编写高质量可保护的代码:高深莫测的正文

招贤纳士

政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。

如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com

退出移动版