我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。

本文作者:琉易 liuxianyu.cn

前段时间分享了《搭建自动化 Web 页面性能检测系统 —— 设计篇》,我们提到了性能检测的一些名词和自研性能检测系统的原因,也简单介绍了一下系统的设计。在这里我们书接上篇记录下是如何实现一个性能检测系统的。

开始前欢迎大家 Star:https://github.com/DTStack/yice-performance

先看下性能检测系统 —— 易测的实现效果:

一、技术选型

服务端框架选择的是 Nestjs,Web 页面选择的是 Vite + React。由于所在团队当前的研发环境已经全面接入自研的 devops 系统,易测收录的页面也是对接 devops 进行检测的。

1.1、整体架构设计

易测的检测服务基于 Lighthouse + Puppeteer 实现。下图是易测的一个整体架构图:

1.2、实现方案

易测的检测流程是:根据子产品的版本获取到待检测的地址、对应的登录地址、用户名和密码,然后通过 Puppeteer 先跳转到对应的登录页面,接着由 Puppeteer 输入用户名、密码、验证码,待登录完成后跳转至待检测的页面,再进行页面性能检测。如果登录后还在登录页,表示登录失败,则获取错误提示并抛出到日志。为了检测方便,检测的均为开发环境且将登录的验证码校验关闭。

以下是易测的检测流程图:

二、Lighthouse

易测通过 Node 模块引入 Lighthouse,不需要登录的页面检测可以直接使用 Lighthouse,基础用法:

const lighthouse = require('lighthouse');
const runResult = await lighthouse(url, lhOptions, lhConfig);

2.1、options

lhOptions 的主要参数有:

{
port: PORT, // chrome 运行的端口
logLevel: 'error',
output: 'html', // 以 html 文件的方式输出报告
onlyCategories: ['performance'], // 仅采集 performance 数据
disableStorageReset: true, // 禁止在运行前清除浏览器缓存和其他存储 API
}

2.2、config

lhConfig 的主要参数有:

{
extends: 'lighthouse:default', // 继承默认配置
settings: {
onlyCategories: ['performance'],
// onlyAudits: ['first-contentful-paint'],
formFactor: 'desktop',
throttling: {
rttMs: 0, // 网络延迟,单位 ms
throughputKbps: 10 * 1024,
cpuSlowdownMultiplier: 1,
requestLatencyMs: 0, // 0 means unset
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
screenEmulation: {
mobile: false,
width: 1440,
height: 960,
deviceScaleFactor: 1,
disabled: false,
},
skipAudits: ['uses-http2'], // 跳过的检查
emulatedUserAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4695.0 Safari/537.36 Chrome-Lighthouse',
},
}

settings 属于 Lighthouse 的运行时配置,主要是用来模拟网络和设备的信息,以及使用到哪些审查器。如果检测的页面有 web 端和 h5 端之分,也是在 settings 进行配置。

检测结果会有总分、各小项的耗时、瀑布图、改进建议等,如下:

三、Puppeteer

需要登录后才能访问的页面涉及到登录、点击等操作,我们需要借助 Puppeteer 来模拟点击。基础用法:

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch(puppeteerConfig);
const page = await browser.newPage();

3.1、puppeteerConfig

{
args: ['--no-sandbox', '--disable-setuid-sandbox', `--remote-debugging-port=${PORT}`],
headless: true, // 是否使用无头浏览器
defaultViewport: { width: 1440, height: 960 }, // 指定打开页面的宽高
slowMo: 15, // 使 Puppeteer 操作减速,可以观察到 Puppeteer 的操作
}

headless 为 false 时方便本地调试,通过调整 slowMo 的大小可以观察到 Puppeteer 的模拟操作。

四、开始检测

4.1、主方法

const taskRun = async (task: ITask, successCallback, failCallback, completeCallback) => {
const { taskId, start, url, loginUrl } = task;
try {
// 依据是否包含 devops 来判断是否需要登录
const needLogin = url.includes('devops') || loginUrl;
console.log(
`\ntaskId: ${taskId}, 本次检测${needLogin ? '' : '不'}需要登录,检测地址:`,
url
); // 需要登录与否会决定使用哪个方法
const runResult = needLogin ? await withLogin(task) : await withOutLogin(task); // 保存检测结果的报告文件,便于预览
const urlStr = url.replace(/http(s?):\/\//g, '').replace(/\/|#/g, '');
const fileName = `${moment().format('YYYY-MM-DD')}-${taskId}-${urlStr}`;
const filePath = `./static/${fileName}.html`;
const reportPath = `/report/${fileName}.html`;
fs.writeFileSync(filePath, runResult?.report); // 整理性能数据
const audits = runResult?.lhr?.audits || {};
const auditRefs =
runResult?.lhr?.categories?.performance?.auditRefs?.filter((item) => item.weight) || [];
const { score = 0 } = runResult?.lhr?.categories?.performance || {}; const performance = [];
for (const auditRef of auditRefs) {
const { weight, acronym } = auditRef;
const { score, numericValue } = audits[auditRef.id] || {};
if (numericValue === undefined) {
throw new Error(
`检测结果出现问题,没有单项检测时长,${JSON.stringify(audits[auditRef.id])}`
);
}
performance.push({
weight,
name: acronym,
score: Math.floor(score * 100),
duration: Math.round(numericValue * 100) / 100,
});
}
const duration = Number((new Date().getTime() - start).toFixed(2)); // 汇总检测结果
const result = {
score: Math.floor(score * 100),
duration,
reportPath,
performance,
}; // 抛出结果
await successCallback(taskId, result); console.log(`taskId: ${taskId}, 本次检测耗时:${duration}ms`);
return result;
} catch (error) {
// 错误处理
const failReason = error.toString().substring(0, 10240);
const duration = Number((new Date().getTime() - start).toFixed(2));
await failCallback(task, failReason, duration);
console.error(`taskId: ${taskId}, taskRun error`, `taskRun error, ${failReason}`);
throw error;
} finally {
completeCallback();
}
};

4.2、不需要登录

const withOutLogin = async (runInfo: ITask) => {
const { taskId, url } = runInfo;
let chrome, runResult;
try {
console.log(`taskId: ${taskId}, 开始检测`); // 通过 API 控制 Node 端的 chrome 打开标签页,借助 Lighthouse 检测页面
chrome = await chromeLauncher.launch(chromeLauncherOptions);
runResult = await lighthouse(url, getLhOptions(chrome.port), lhConfig); console.log(`taskId: ${taskId}, 检测完成,开始整理数据`);
} catch (error) {
console.error(`taskId: ${taskId}, 检测失败`, `检测失败,${error?.toString()}`);
throw error;
} finally {
await chrome.kill();
} return runResult;
};

4.3、需要登录

const withLogin = async (runInfo: ITask) => {
const { taskId, url } = runInfo; // 创建 puppeteer 无头浏览器
const browser = await puppeteer.launch(getPuppeteerConfig(PORT));
const page = await browser.newPage(); let runResult;
try {
// 登录
await toLogin(page, runInfo);
// 选择租户
await changeTenant(page, taskId); console.log(`taskId: ${taskId}, 准备工作完成,开始检测`); // 开始检测
runResult = await lighthouse(url, getLhOptions(PORT), lhConfig); console.log(`taskId: ${taskId}, 检测完成,开始整理数据`);
} catch (error) {
console.error(`taskId: ${taskId}, 检测出错`, `${error?.toString()}`);
throw error;
} finally {
// 检测结束关闭标签页、无头浏览器
await page.close();
await browser.close();
} return runResult;
};

4.4、模拟登录

所在团队的子产品均需要登录后才能访问,且每次检测打开的都是类似无痕浏览器的标签页,不存在登录信息的缓存,所以每次检测这些页面前需要完成登录操作:

const toLogin = async (page, runInfo: ITask) => {
const { taskId, loginUrl, username, password } = runInfo;
try {
await page.goto(loginUrl);
// 等待指定的选择器匹配元素出现在页面中
await page.waitForSelector('#username', { visible: true }); // 用户名、密码、验证码
const usernameInput = await page.$('#username');
await usernameInput.type(username);
const passwordInput = await page.$('#password');
await passwordInput.type(password);
const codeInput = await page.$('.c-login__container__form__code__input');
await codeInput.type('bz4x'); // 登录按钮
await page.click('.c-login__container__form__btn');
// await page.waitForNavigation();
await sleep(Number(process.env.RESPONSE_SLEEP || 0) * 2); const currentUrl = await page.url();
// 依据是否包含 login 来判断是否需要登录,若跳转之后仍在登录页,说明登录出错
if (currentUrl.includes('login')) {
throw new Error(`taskId: ${taskId}, 登录失败,仍在登录页面`);
} else {
console.log(`taskId: ${taskId}, 登录成功`);
}
} catch (error) {
console.error(`taskId: ${taskId}, 登录出错`, error?.toString());
throw error;
}
};

4.5、得分落库

等待所有的检测步骤都完成后,在 successCallback 方法中处理检测数据,此时可根据不同的性能指标计算得出最终得分和小项得分,统一落库。

五、自动检测

除了可以在页面手动触发检测,易测主要使用的是自动检测。自动检测的目的是方便统计所有子产品的性能趋势,便于分析各版本间的性能变化,以及子产品间的性能优劣,最终得出优化方向。

5.1、任务主动调度

易测试运行阶段,由于使用的是开发环境进行检测,所以将自动检测时间设置为工作时间的间隙,减少影响检测结果的干扰因素,后续正式部署后,也将调低检测的频率。

自动检测可以主动进行任务的调度,也可以手动触发任务,借助 @nestjs/schedule 实现定时任务:

import { Cron } from '@nestjs/schedule';

export class TaskRunService {
// 每分钟执行一次 https://docs.nestjs.com/techniques/task-scheduling#declarative-cron-jobs
@Cron('0 * * * * *')
async handleCron() {
// 检测版本的 cron 符合当前时间运行的则创建任务
process.env.NODE_ENV === 'production' && this.checkCronForCurrentDate();
}
}

5.2、失败告警

检测失败会有钉钉通知,点击可快速跳转至易测内查看具体原因。

5.3、性能趋势图

由下方的趋势图简单分析后,可以得出子产品版本间的性能变化。

六、对接内部系统

6.1、对接 Jenkins

所在团队的子产品在版本间做了一些脚手架的封装升级,对接 Jenkins 就可以采集到各个版本间构建时长和构建后的文件大小等信息的变化,有助于性能相关数据的汇总、脚手架的分析改进。

在 Jenkins 的构建回调里,处理后可以拿到构建时长和构建后的文件大小等信息,由 Jenkins 调用易测提供的接口,按分支处理好版本后将数据落库,在易测中展示出来。

七、结尾

如果你也准备搭建一个自己团队的检测系统,可以参考下易测的设计思路,希望这两篇文章对你的工作有所助力。

完成上述工作后,接下来需要考虑的有易测功能的权限控制、数据分析、如何根据业务场景进行检测等方面。毕竟 Lighthouse 检测的一般是单个页面,而业务场景一般是工作流程的编排即流程的整体操作。

最后,欢迎大家不吝 Star:https://github.com/DTStack/yice-performance


最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star

搭建自动化 Web 页面性能检测系统 —— 实现篇的更多相关文章

  1. Grunt搭建自动化web前端开发环境--完整流程

    Grunt搭建自动化web前端开发环境-完整流程 jQuery在使用grunt,bootstrap在使用grunt,百度UEditor在使用grunt,你没有理由不学.不用! 1. 前言 各位web前 ...

  2. 隔壁老主精讲web页面性能优化。

    首先说一下为什么要进行web页面性能优化,在同样的网络环境下,两个同样能满足你的需求的网站,一个“Biu”的一下就加载出来了,一个卡--卡--卡--卡--卡--才出来,你会选择哪个?研究表明:用户最满 ...

  3. Web页面性能优化(YSlow)

    YSlow(解析为Why Slow)是雅虎基于网站优化规则推出的工具,帮助你分析并优化网站性能.旧版Yslow 有13条规则,新版Yslow有23项规则,YSlow会根据这些规则分析你的网站,并给出评 ...

  4. 好用的前端页面性能检测工具—sitespeed.io

    引言 最近在做HTTP2技术相关调研,想确认一下HTTP2在什么情境下性能会比HTTP1.x有显著提升,当我把http2的本地环境(nginx+PHP)部署完成后进行相关测试时,我遇到了以下问题: ( ...

  5. web页面性能优化

    web前端页面性能优化 网站的划分一般为二:前端和后台.我们可以理解成后台是用来实现网站的功能的,比如:实现用户注册,用户能够为文章发表评论等等.而前端呢? 其实应该是属于功能的表现.并且影响用户访问 ...

  6. base64:URL背景图片与web页面性能优化

    一.base64百科 Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,可用于在HTTP环境下传递较长的标识信息. 某人: 唉,我彻底废柴了,为何上面明明是中文,洒家却看不懂嘞,为什 ...

  7. 小tip: base64:URL背景图片与web页面性能优化——张鑫旭

    一.base64百科 Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,可用于在HTTP环境下传递较长的标识信息. 某人: 唉,我彻底废柴了,为何上面明明是中文,洒家却看不懂嘞,为什 ...

  8. 小tip: base64:URL背景图片与web页面性能优化

    转自:http://www.zhangxinxu.com/wordpress/?p=2341 一.base64百科 Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,可用于在HTTP ...

  9. Web前端性能优化进阶——完结篇

    前言 在之前的文章 如何优化网站性能,提高页面加载速度 中,我们简单介绍了网站性能优化的重要性以及几种网站性能优化的方法(没有看过的可以狂戳 链接 移步过去看一下),那么今天我们深入讨论如何进一步优化 ...

  10. 提高Web页面性能的技巧

    现在动辄几兆大小的页面加载量,让性能优化成了不可避免的热门话题.WEB 应用越流畅,用户体验就会越好,继而带来更多的访问量.这也就是说,我们应该反省一下那些过度美化的 CSS3 动画和多重操作的 DO ...

随机推荐

  1. Docker 容器上部署 Zabbix

    首先,从 Docker Hub 上拉取 Zabbix 镜像.可以使用以下命令: docker pull zabbix/zabbix-server-mysql:latest 这会下载最新版本的 Zabb ...

  2. python程序,实现以管理员方式运行程序,也就是提升程序权限

    quest UAC elevation from within a Python script? 我希望我的Python脚本能够在Vista上复制文件. 当我从普通的cmd.exe窗口运行它时,不会生 ...

  3. .Net Core后端架构实战【2-实现动态路由与Dynamic API】

    摘要:基于.NET Core 7.0WebApi后端架构实战[2-实现动态路由与Dynamic API]  2023/02/22, ASP.NET Core 7.0, VS2022 引言 使用过ABP ...

  4. 四月二十日java基础知识

    1.不可被继承的成员与最终类:在默认情况下,所有的成员变量和成员方法都可以被覆盖,如果父类的成员不希望被子类的成员锁覆盖可以将它们声明为final.如果用final来修饰成员变量,则说明该成员变量是最 ...

  5. Vue 环境准备

    近期接触了下前端项目,记录下学习过程. 近几年前端发展的迅猛,各种框架层出不穷,vue react angular ,各种第三方组件 原来会点js,jQuery 前后端一个人全搞定了,现在前后端分离, ...

  6. LeeCode 动态规划(三)

    完全背包问题 题目描述 有 n 件物品和容量为 w 的背包,给你两个数组 weights 和 values,分别表示第 i 件物品的重量和价值,每件物品可以放入多次,求解将哪些物品装入背包可使得物品价 ...

  7. 【Vue2.x源码系列07】监听器watch原理

    上一章 Vue2计算属性原理,我们介绍了计算属性是如何实现的?计算属性缓存原理?以及洋葱模型是如何应用的? 本章目标 监听器是如何实现的? 监听器选项 - immediate.deep 内部实现 初始 ...

  8. MySQL概述与安装

    MySQL数据库 概要: 一.MySQL数据库的概述 二.MySQL数据库的搭建 三.MySQL数据库软件的使用 四.MySQL数据类型 五.MySQL数据库数据的操作 一.初始MySQL数据库 1. ...

  9. [C++提高编程] 3.7 list容器

    文章目录 3.7 list容器 3.7.1 list基本概念 3.7.2 list构造函数 3.7.3 list 赋值和交换 3.7.4 list 大小操作 3.7.5 list 插入和删除 3.7. ...

  10. Prism Sample 22-ConfirmCancelNavigation

    导航到一个视图,如果在离开这个视图时需要确认,在VM中实现以下接口 public class ViewAViewModel : BindableBase, IConfirmNavigationRequ ...