政采云前端团队博客:如何从0到1搭建性能检测系统

优采云 发布时间: 2021-08-04 19:17

  政采云前端团队博客:如何从0到1搭建性能检测系统

  本文首发于郑才云前端团队的博客:如何构建一个从0到1的性能检测系统

  前言

  前端页面性能对用户留存和用户直观体验有重要影响。当页面加载时间超过2秒时,加载时间每增加一秒就会导致大量用户流失。因此,优化页面性能无疑是有益的。 网站 是非常重要的一步。

  我们如何知道一个页面的性能?了解页面性能后如何优化?一个页面有很多性能指标。面对众多的性能指标,老手可能一时不知从何下手分析。而且,不同的团队有不同的业务,绩效分析的指标不能一概而论。比如一般的电商网站,肯定有很多图片,图片加载的性能提升对网站的性能提升影响更大。对于一些由表单组成的中台页面,提高图片加载速度的好处远不及电商网站。

  综上所述,不同的团队有各自不同的业务,业务之间存在巨大差异,绩效指标不能一概而论。因此,用统一的检测模型覆盖所有场景是不现实的。本文将介绍如何定制属于您团队的性能测试平台。

  先看郑彩云-百色的性能测试平台

  

  在谈论性能指标之前,让我们先谈谈 Lighthouse。

  灯塔

  Lighthouse 是一种开源自动化工具,用于分析和改进 Web 应用程序的质量。运行 Lighthouse 有 4 种方式,分别在 Chrome 开发者工具、Chrome 扩展、Node CLI 和 Node 模块中。 Baice主要基于Node模块方式,并在此基础上进行扩展和开发。 Lighthouse的详细使用请参考Git:

  下图是 Lighthouse 测试页面性能的最终结果。可以看到指标其实还是比较齐全的。

  

  可能有人会问,为什么不直接使用Lighthouse。首先,由于莫名其妙的原因,在国内直接使用Chrome开发者工具中的Lighthouse时,会一直处于Lighthouse正在预热的状态。其次,Chrome 扩展程序不支持需要登录的页面。最后,对于前言中的一些定制需求,Lighthouse 无法完全满足,所以需要基于Lighthouse 进行定制,构建满足业务需求的性能测试平台。

  整体设计框架

  下图为百测系统整体架构

  

  Baice采集page 性能数据处理

  百策系统监控页面的主要方式是综合监控。什么是综合监控,可以参考这个文章:蚂蚁金服如何把前端性能监控做到极致(*Lukk5Ufhy)。综上所述,综合监控的优势在于:采集可以拥有更丰富的数据,可以根据不同的场景定制不同的运行环境。首先,百测根据不同的场景开发了不同的检测模型,比如正财云首页、正财云众泰页面。其次,百测的主要目标是提升页面性能,需要保证环境和硬件条件与页面性能一致,所以选择综合监控比较合适。

  先看Chrome Lighthouse的架构图(该图来自Lighthouse Git),主要基于4个主要步骤实现,分别是交互驱动、采集、审计和记录合成,参考Chrome Lighthouse,Baice的检测模型逻辑也主要由这4个步骤组成:

  1、 页面交互后,发起调用服务的请求。

  2、遍历当前页面需要的采集器,合并为一个总采集器,和采集数据。

  3、对第二步采集收到的数据进行性能计算和打分。

  4、将性能测试结果存入数据库。

  

  Baice采集page绩效数据实施方案

  百思实现页面性能数据采集的方案主要依靠无头浏览器Puppeteer结合Lighthouse。 Puppeteer 是 Chrome 团队提供的无接口 Chrome 工具,称为 Headless 浏览器,通过 API 控制 Node 端的 Chrome。 Baice的主要逻辑是启动一个不需要在服务器上显示的Chrome,通过Lighthouse的API新建一个tab并打开,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(#?product=Puppeteer&version=v5.3.0&show=api-puppeteerlaunchoptions)。可以设置headless为false查看浏览器的创建和页面的创建,本地调试即可。

  /**

  * 登录前准备工作,创建浏览器和页面

  *

  * @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模拟输入用户名和密码,模拟点击登录Button。根据同一浏览器下共享同域名cookie的特点,新开一个标签页打开需要检测的网址,然后就可以开始性能测试了。

  ○ 打开页面

  如何在Puppeteer中使用Lighthouse可以参考Using Puppeteer with Lighthouse ()。以下代码主要检测网页在桌面上的表现,改变检测环境的功能将在以后发布:可以根据网页的域名判断网页是手机还是电脑正彩云,并根据不同的系统环境切换到不同的浏览器参数。

  /**

  * 在 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;

}

  ○ 挂钩功能

  钩子函数实际上是一个抽象类。在运行不同的聚会时,相应的类会实现抽象类。钩子函数的主要作用是注册不同时期的回调。主要有两个钩子函数,beforePass 和 afterPass。 beforePass 的作用主要是在页面加载前注册一些*敏*感*词*器。比如想要获取页面加载后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();

}

  ○ 采集器的实现

  Baice共有6个采集器,分别是Domstats Gathering、Image Elements Gathering、Lighthouse Gathering、Metrics Gathering、Network Recorder Gathering和Performance Gathering。

  每个采集器都会实现一个特定的采集功能:

  以Domstats Gathering为例,详细说明如何获取页面检查数据。首先实现抽象类的两个方法:beforePass和afterPass。 beforePass 的实现逻辑是给页面对象添加一个 domcontentloaded 时间点监控方法。监控方法的主要功能是判断文档是否有水平滚动条。 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 {

  // 根据每条 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  {

        // 调用检测接口记录性能评分

        await this.report();

      });

      // 每周五18:00发送周报

      schedule.scheduleJob(`hawkeye-weekly-send`, '0 0 18 * * 5', async () => {

        // 发送邮件的具体实现方法,主要通过 ejs 渲染模版,通过 nodemailer 发送邮件

        await this.send();

       });

    }

  }

}

  

  ○ 联系鲁班

  鲁班是什么,可以参考这个文章:,一句话概括,可以说鲁班是正财云的页面搭建系统。

  与鲁班对接时,主要包括鲁班页面性能数据的录入和鲁班页面的录入(用于后续每周定期检查)。

  结束

  如果你也想搭建自己的性能测试平台,偶然看到这个文章,希望这篇文章对你有所帮助。

  本文主要讲如何搭建性能平台。当你已经能够搭建一个性能平台时,你不妨考虑一下业务页面的检测模型。

  看完两件事

  如果你觉得这个内容对你很有启发,我想请你帮我做两件小事

  1.点击“看”让更多人看到这个内容(点击“看”,bug -1 ????)

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线