如何使用 GitLab CI 和 GitLab Artifacts 的 Hoodoo 使性能可见
已发表: 2022-03-10性能下降是我们每天都面临的问题。 我们可以努力使应用程序快速运行,但我们很快就结束了我们开始的地方。 之所以会发生这种情况,是因为添加了新功能,而且我们有时对我们不断添加和更新的包没有重新考虑,或者考虑我们代码的复杂性。 这通常是一件小事,但仍然是关于小事的。
我们无法承受缓慢的应用程序。 绩效是可以带来和留住客户的竞争优势。 我们不能经常花时间重新优化应用程序。 它昂贵且复杂。 这意味着,尽管从业务角度来看性能的所有好处,但它几乎没有盈利。 作为为任何问题提出解决方案的第一步,我们需要让问题可见。 本文将帮助您解决这个问题。
注意:如果您对 Node.js 有基本的了解,对 CI/CD 的工作方式有一个模糊的概念,并且关心应用程序的性能或它可以带来的业务优势,那么我们很高兴。
如何为项目创建绩效预算
我们应该问自己的第一个问题是:
“什么是高性能项目?”
“我应该使用哪些指标?”
“这些指标的哪些值是可以接受的?”
指标选择超出了本文的范围,并且高度依赖于项目上下文,但我建议您从阅读 Philip Walton 的以用户为中心的性能指标开始。
从我的角度来看,使用库的大小(以千字节为单位)作为 npm 包的度量是一个好主意。 为什么? 嗯,这是因为如果其他人将您的代码包含在他们的项目中,他们可能希望尽量减少您的代码对其应用程序最终大小的影响。
对于该站点,我会考虑第一个字节的时间 (TTFB) 作为一个指标。 该指标显示服务器响应某些内容所需的时间。 这个指标很重要,但很模糊,因为它可以包括任何东西——从服务器渲染时间开始,到延迟问题结束。 因此,最好将它与 Server Timing 或 OpenTracing 结合使用,以找出它的确切组成部分。
您还应该考虑诸如交互时间 (TTI) 和首次有意义的绘制(后者很快将被最大内容绘制 (LCP) 取代)等指标。 我认为这两个都是最重要的——从感知性能的角度来看。
但请记住:指标总是与上下文相关的,所以请不要认为这是理所当然的。 想想在你的具体情况下什么是重要的。
定义指标所需值的最简单方法是使用您的竞争对手——甚至是您自己。 此外,有时,诸如性能预算计算器之类的工具可能会派上用场 - 只需稍微尝试一下即可。
性能下降是我们每天都面临的问题。 我们可以努力使应用程序快速运行,但很快我们就结束了我们开始的地方。
“
利用竞争对手为您谋取利益
如果你碰巧从一只欣喜若狂的过度兴奋的熊身边逃跑,那么你已经知道,你不需要成为奥运会冠军就可以摆脱这个麻烦。 你只需要比其他人快一点。
所以制作一个竞争对手名单。 如果这些是相同类型的项目,那么它们通常由彼此相似的页面类型组成。 例如,对于一个网店来说,它可能是一个包含产品列表、产品详细信息页面、购物车、结帐等的页面。
- 衡量您在竞争对手项目的每种类型页面上选择的指标的价值;
- 在您的项目中衡量相同的指标;
- 为竞争对手的项目中的每个指标找到比您的价值更接近的指标。 为他们增加 20% 并设定为您的下一个目标。
为什么是 20%? 这是一个神奇的数字,据说这意味着肉眼可以看到差异。 您可以在 Denys Mishunov 的文章“为什么感知性能很重要,第 1 部分:时间感知”中阅读更多关于这个数字的信息。
与阴影的战斗
你有一个独特的项目吗? 没有竞争对手? 或者你已经在所有可能的意义上比他们中的任何一个都好? 这不是问题。 你总是可以与唯一有价值的对手竞争,即你自己。 在每种类型的页面上衡量项目的每个性能指标,然后将它们提高 20%。
综合测试
有两种衡量性能的方法:
- 合成(在受控环境中)
- RUM (真实用户测量)
正在从生产中的真实用户那里收集数据。
在本文中,我们将使用综合测试并假设我们的项目使用带有内置 CI 的 GitLab 进行项目部署。
图书馆及其大小作为衡量标准
假设您决定开发一个库并将其发布到 NPM。 您希望它保持轻便——比竞争对手轻得多——因此它对最终项目的最终尺寸的影响较小。 这可以节省客户流量——有时是客户支付的流量。 它还允许项目更快地加载,这对于不断增长的移动份额和连接速度慢和互联网覆盖分散的新市场而言非常重要。
测量库大小的包
为了使库的大小尽可能小,我们需要仔细观察它随着开发时间的变化。 但是你怎么能做到呢? 好吧,我们可以使用来自 Evil Martians 的 Andrey Sitnik 创建的包大小限制。
让我们安装它。
npm i -D size-limit @size-limit/preset-small-lib
然后,将其添加到package.json
。
"scripts": { + "size": "size-limit", "test": "jest && eslint ." }, + "size-limit": [ + { + "path": "index.js" + } + ],
"size-limit":[{},{},…]
块包含我们要检查的文件大小的列表。 在我们的例子中,它只是一个文件: index.js
。
NPM 脚本size
只是运行size-limit
包,它读取前面提到的配置块size-limit
并检查那里列出的文件的大小。 让我们运行它,看看会发生什么:
npm run size

我们可以看到文件的大小,但这个大小实际上是不受控制的。 让我们通过向package.json
添加limit
来解决这个问题:
"size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],
现在,如果我们运行脚本,它将根据我们设置的限制进行验证。

如果新开发将文件大小更改为超过定义的限制,脚本将以非零代码完成。 除了其他事情之外,这意味着它将停止 GitLab CI 中的管道。

现在我们可以使用 git hook 在每次提交之前检查文件大小是否符合限制。 我们甚至可以使用 husky 包以一种简单易用的方式制作它。
让我们安装它。
npm i -D husky
然后,修改我们的package.json
。
"size-limit": [ { "limit": "2 KB", "path": "index.js" } ], + "husky": { + "hooks": { + "pre-commit": "npm run size" + } + },
现在在每次提交自动执行之前npm run size
命令,如果它以非零代码结束,那么提交将永远不会发生。

但是有很多方法可以跳过钩子(故意甚至是偶然),所以我们不应该过分依赖它们。
此外,重要的是要注意我们不需要进行此检查阻塞。 为什么? 因为当您添加新功能时,库的大小会增长是可以的。 我们需要使更改可见,仅此而已。 这将有助于避免由于引入了我们不需要的帮助程序库而导致的意外大小增加。 而且,也许,让开发人员和产品所有者有理由考虑所添加的功能是否值得增加尺寸。 或者,也许,是否有更小的替代包。 Bundlephobia 允许我们为几乎所有 NPM 包找到替代方案。
那么我们应该怎么做呢? 让我们直接在合并请求中显示文件大小的变化! 但是你不要直接推到掌握; 你表现得像个成熟的开发者,对吧?
在 GitLab CI 上运行我们的检查
让我们添加一个度量类型的 GitLab 工件。 工件是一个文件,它将在管道操作完成后“存活”。 这种特定类型的工件允许我们在合并请求中显示一个额外的小部件,显示 master 和功能分支中的工件之间度量值的任何变化。 metrics
工件的格式是文本 Prometheus 格式。 对于工件内的 GitLab 值,它只是文本。 GitLab 不了解值的确切变化是什么——它只知道值不同。 那么,我们具体应该怎么做呢?
- 在管道中定义工件。
- 更改脚本,以便它在管道上创建工件。
要创建工件,我们需要以这种方式更改.gitlab-ci.yml
:
image: node:latest stages: - performance sizecheck: stage: performance before_script: - npm ci script: - npm run size + artifacts: + expire_in: 7 days + paths: + - metric.txt + reports: + metrics: metric.txt
-
expire_in: 7 days
— 工件将存在 7 天。 paths: metric.txt
它将保存在根目录中。 如果您跳过此选项,则无法下载它。reports: metrics: metric.txt
该工件将具有reports:metrics
现在让我们让 Size Limit 生成一个报告。 为此,我们需要更改package.json
:
"scripts": { - "size": "size-limit", + "size": "size-limit --json > size-limit.json", "test": "jest && eslint ." },
size-limit
with key --json
将以 json 格式输出数据:

size-limit --json
JSON 输出到控制台。 JSON 包含一个对象数组,其中包含文件名和大小,并让我们知道它是否超过了大小限制。 (大预览) 并且 redirection > size-limit.json
会将 JSON 保存到文件size-limit.json
中。
现在我们需要从中创建一个工件。 格式归结为[metrics name][space][metrics value]
。 让我们创建脚本generate-metric.js
:
const report = require('./size-limit.json'); process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}Kb`); process.exit(0);
并将其添加到package.json
:
"scripts": { "size": "size-limit --json > size-limit.json", + "postsize": "node generate-metric.js > metric.txt", "test": "jest && eslint ." },
因为我们使用了post
前缀,所以npm run size
命令将首先运行size
脚本,然后自动执行postsize
脚本,这将导致我们的工件metric.txt
文件的创建。
结果,当我们将这个分支合并到 master 时,改变一些东西并创建一个新的合并请求,我们将看到以下内容:

在出现在页面上的小部件中,我们首先看到度量的名称( size
),然后是特征分支中度量的值以及圆括号内的主值。
现在我们实际上可以看到如何更改包的大小并做出是否应该合并它的合理决定。
- 您可能会在此存储库中看到所有这些代码。
恢复
好的! 所以,我们已经想出了如何处理这种琐碎的情况。 如果您有多个文件,只需使用换行符分隔指标。 作为 Size Limit 的替代方案,您可以考虑 bundlesize。 如果您使用的是 WebPack,则可以通过使用--profile
和--json
标志构建所需的所有大小:
webpack --profile --json > stats.json
如果您使用的是 next.js,则可以使用 @next/bundle-analyzer 插件。 由你决定!
使用灯塔
Lighthouse 是项目分析中的事实标准。 让我们编写一个脚本,让我们能够衡量性能、a11y、最佳实践,并为我们提供 SEO 分数。
测量所有东西的脚本
首先,我们需要安装将进行测量的灯塔包。 我们还需要安装 puppeteer,我们将把它用作无头浏览器。
npm i -D lighthouse puppeteer
接下来,让我们创建一个lighthouse.js
脚本并启动我们的浏览器:
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); })();
现在让我们编写一个函数来帮助我们分析给定的 URL:
const lighthouse = require('lighthouse'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => { const data = await lighthouse( `${DOMAIN}${url}`, { port: new URL(browser.wsEndpoint()).port, output: 'json', }, { extends: 'lighthouse:full', } ); const { report: reportJSON } = data; const report = JSON.parse(reportJSON); // … }
伟大的! 我们现在有一个函数,它将接受浏览器对象作为参数并返回一个函数,该函数将接受URL
作为参数,并在将该URL
传递给lighthouse
后生成报告。
我们将以下参数传递给lighthouse
:
- 我们要分析的地址;
-
lighthouse
选项,特别是浏览器port
和output
(报告的输出格式); -
report
配置和lighthouse:full
(我们可以测量的所有内容)。 如需更精确的配置,请查看文档。
精彩的! 我们现在有了我们的报告。 但是我们能用它做什么呢? 好吧,我们可以根据限制检查指标并使用非零代码退出脚本,这将停止管道:
if (report.categories.performance.score < 0.8) process.exit(1);
但我们只是想让性能可见且无阻塞? 那么让我们采用另一种神器类型:GitLab 性能神器。
GitLab 性能神器
为了理解这种工件格式,我们必须阅读 sitespeed.io 插件的代码。 (为什么 GitLab 不能在他们自己的文档中描述他们的工件的格式?神秘。 )
[ { "subject":"/", "metrics":[ { "name":"Transfer Size (KB)", "value":"19.5", "desiredSize":"smaller" }, { "name":"Total Score", "value":92, "desiredSize":"larger" }, {…} ] }, {…} ]
工件是包含对象数组的JSON
文件。 它们中的每一个都代表一个关于一个URL
的报告。
[{page 1}, {page 2}, …]
每个页面由具有以下属性的对象表示:
-
subject
页面标识符(使用这样的路径名非常方便); -
metrics
对象数组(每个对象代表在页面上进行的一次测量)。
{ "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }
measurement
是包含以下属性的对象:
-
name
测量名称,例如它可能是Time to first byte
或Time to interactive
。 -
value
数值测量结果。 -
desiredSize
如果目标值应该尽可能小,例如Time to interactive
度量,那么该值应该smaller
。 如果它应该尽可能大,例如对于灯塔Performance score
,则使用larger
。
{ "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }
让我们修改我们的buildReport
函数,让它返回一个带有标准灯塔指标的页面的报告。

const buildReport = browser => async url => { // … const metrics = [ { name: report.categories.performance.title, value: report.categories.performance.score, desiredSize: 'larger', }, { name: report.categories.accessibility.title, value: report.categories.accessibility.score, desiredSize: 'larger', }, { name: report.categories['best-practices'].title, value: report.categories['best-practices'].score, desiredSize: 'larger', }, { name: report.categories.seo.title, value: report.categories.seo.score, desiredSize: 'larger', }, { name: report.categories.pwa.title, value: report.categories.pwa.score, desiredSize: 'larger', }, ]; return { subject: url, metrics: metrics, }; }
现在,当我们有一个生成报告的函数时。 让我们将它应用于项目的每种类型的页面。 首先,我需要声明process.env.DOMAIN
应该包含一个暂存域(您需要事先从功能分支部署项目到该暂存域)。
+ const fs = require('fs'); const lighthouse = require('lighthouse'); const puppeteer = require('puppeteer'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => {/* … */}; + const urls = [ + '/inloggen', + '/wachtwoord-herstellen-otp', + '/lp/service', + '/send-request-to/ww-tammer', + '/post-service-request/binnenschilderwerk', + ]; (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + const metrics = await builder(url); + report.push(metrics); + } + fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + await browser.close(); })();
- 您可以在此存储库中的此要点和工作示例中找到完整的源代码。
注意:此时,你可能想打断我并徒劳地尖叫,“你为什么要占用我的时间——你甚至不能正确使用 Promise.all!” 在我的辩护中,我敢说,不建议同时运行多个灯塔实例,因为这会对测量结果的准确性产生不利影响。 另外,如果你不表现出应有的聪明才智,就会导致异常。
使用多个进程
您还在进行并行测量吗? 好吧,您可能想要使用节点集群(如果您喜欢大胆的话,甚至可以使用工作线程),但是只有在您的管道在具有多个可用 cors 的环境中运行时才讨论它是有意义的。 即使这样,您也应该记住,由于 Node.js 的性质,您将在每个进程分叉中生成全权重的 Node.js 实例(而不是重复使用会导致 RAM 消耗增加的同一个实例)。 所有这一切都意味着,由于硬件需求的增长,它的成本会更高,而且速度会更快。 看来这场比赛得不偿失。
如果你想承担这个风险,你需要:
- 按核心数将 URL 数组拆分为块;
- 根据核数创建一个进程的fork;
- 将数组的一部分传输到分支,然后检索生成的报告。
要拆分数组,您可以使用多堆方法。 下面的代码——只用了几分钟的时间——不会比其他代码差:
/** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; }
/** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; }
根据核心数制作分叉:
// Adding packages that allow us to use cluster const cluster = require('cluster'); // And find out how many cors are available. Both packages are build-in for node.js. const numCPUs = require('os').cpus().length; (async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { // Creating child processes const worker = cluster.fork(); }); } else { // Child process } })();
让我们将一组块传输到子进程并检索报告:

(async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { const worker = cluster.fork(); + // Send message with URL's array to child process + worker.send(chunk); }); } else { // Child process + // Recieveing message from parent proccess + process.on('message', async (urls) => { + const browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], + }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + // Generating report for each URL + const metrics = await builder(url); + report.push(metrics); + } + // Send array of reports back to the parent proccess + cluster.worker.send(report); + await browser.close(); + }); } })();
最后,将报告重新组合到一个数组中并生成一个工件。
- 通过一个示例查看完整的代码和存储库,该示例展示了如何将 lighthouse 与多个进程一起使用。
测量精度
好吧,我们将测量并行化,这增加了lighthouse
已经不幸的大测量误差。 但是我们如何减少它呢? 好吧,进行一些测量并计算平均值。
为此,我们将编写一个函数来计算当前测量结果与先前测量结果之间的平均值。
// Count of measurements we want to make const MEASURES_COUNT = 3; /* * Reducer which will calculate an avarage value of all page measurements * @param pages {Object} — accumulator * @param page {Object} — page * @return {Object} — page with avarage metrics values */ const mergeMetrics = (pages, page) => { if (!pages) return page; return { subject: pages.subject, metrics: pages.metrics.map((measure, index) => { let value = (measure.value + page.metrics[index].value)/2; value = +value.toFixed(2); return { ...measure, value, } }), } }
然后,更改我们的代码以使用它们:
process.on('message', async (urls) => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); const builder = buildReport(browser); const report = []; for (let url of urls) { + // Let's measure MEASURES_COUNT times and calculate the avarage + let measures = []; + let index = MEASURES_COUNT; + while(index--){ const metric = await builder(url); + measures.push(metric); + } + const measure = measures.reduce(mergeMetrics); report.push(measure); } cluster.worker.send(report); await browser.close(); }); }
- 通过示例查看包含完整代码和存储库的要点。
现在我们可以将lighthouse
添加到管道中。
将其添加到管道
首先,创建一个名为.gitlab-ci.yml
的配置文件。
image: node:latest stages: # You need to deploy a project to staging and put the staging domain name # into the environment variable DOMAIN. But this is beyond the scope of this article, # primarily because it is very dependent on your specific project. # - deploy # - performance lighthouse: stage: performance before_script: - apt-get update - apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget - npm ci script: - node lighthouse.js artifacts: expire_in: 7 days paths: - performance.json reports: performance: performance.json
puppeteer
需要多个安装的软件包。 作为替代方案,您可以考虑使用docker
。 除此之外,我们将工件的类型设置为性能这一事实是有意义的。 而且,一旦 master 和 feature 分支都拥有它,您将在合并请求中看到这样的小部件:

好的?
恢复
我们终于完成了一个更复杂的案例。 显然,除了灯塔之外,还有多种类似的工具。 例如,sitespeed.io。 GitLab 文档甚至包含一篇文章,解释了如何在 GitLab 的管道中使用sitespeed
。 还有一个用于 GitLab 的插件,它允许我们生成一个工件。 但是谁会更喜欢由社区驱动的开源产品而不是企业怪物拥有的产品呢?
恶人不休息
看起来我们终于到了,但不,还没有。 如果您使用的是付费 GitLab 版本,则计划中包含报告类型metrics
和performance
的工件,从premium
版和silver
版开始,每位用户每月花费 19 美元。 此外,您不能只购买您需要的特定功能 - 您只能更改计划。 对不起。 那么我们能做些什么呢? 与 GitHub 的 Checks API 和 Status API 不同,GitLab 不允许您自己在合并请求中创建实际的小部件。 并且没有希望很快得到它们。

检查您是否确实支持这些功能的一种方法:您可以在管道中搜索环境变量GITLAB_FEATURES
。 如果列表中缺少merge_request_performance_metrics
和metrics_reports
,则不支持此功能。
GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics, elastic_search, export_issues,group_bulk_edit,group_burndown_charts,group_webhooks, issuable_default_templates,issue_board_focus_mode,issue_weights,jenkins_integration, ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees, multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users, push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board, usage_quotas,visual_review_app,wip_limits
如果没有支持,我们需要想出一些办法。 例如,我们可能会在合并请求中添加评论,对表格进行评论,其中包含我们需要的所有数据。 我们可以保持我们的代码不变——将创建工件,但小部件将始终显示一条消息«metrics are unchanged»
。
非常奇怪和不明显的行为; 我必须仔细思考才能理解发生了什么。
那么,有什么计划呢?
- 我们需要从
master
分支读取工件; - 以
markdown
格式创建评论; - 获取从当前特性分支到master的合并请求的标识;
- 添加评论。
如何从主分支读取工件
如果我们想展示性能指标在master
和 feature 分支之间是如何变化的,我们需要从master
读取 artifact。 为此,我们需要使用fetch
。
npm i -S isomorphic-fetch
// You can use predefined CI environment variables // @see https://gitlab.com/help/ci/variables/predefined_variables.md // We need fetch polyfill for node.js const fetch = require('isomorphic-fetch'); // GitLab domain const GITLAB_DOMAIN = process.env.CI_SERVER_HOST || process.env.GITLAB_DOMAIN || 'gitlab.com'; // User or organization name const NAME_SPACE = process.env.CI_PROJECT_NAMESPACE || process.env.PROJECT_NAMESPACE || 'silentimp'; // Repo name const PROJECT = process.env.CI_PROJECT_NAME || process.env.PROJECT_NAME || 'lighthouse-comments'; // Name of the job, which create an artifact const JOB_NAME = process.env.CI_JOB_NAME || process.env.JOB_NAME || 'lighthouse'; /* * Returns an artifact * * @param name {String} - artifact file name * @return {Object} - object with performance artifact * @throw {Error} - thhrow an error, if artifact contain string, that can't be parsed as a JSON. Or in case of fetch errors. */ const getArtifact = async name => { const response = await fetch(`https://${GITLAB_DOMAIN}/${NAME_SPACE}/${PROJECT}/-/jobs/artifacts/master/raw/${name}?job=${JOB_NAME}`); if (!response.ok) throw new Error('Artifact not found'); const data = await response.json(); return data; };
创建评论文本
我们需要以markdown
格式构建评论文本。 让我们创建一些对我们有帮助的服务函数:
/** * Return part of report for specific page * * @param report {Object} — report * @param subject {String} — subject, that allow find specific page * @return {Object} — page report */ const getPage = (report, subject) => report.find(item => (item.subject === subject)); /** * Return specific metric for the page * * @param page {Object} — page * @param name {String} — metrics name * @return {Object} — metric */ const getMetric = (page, name) => page.metrics.find(item => item.name === name); /** * Return table cell for desired metric * * @param branch {Object} - report from feature branch * @param master {Object} - report from master branch * @param name {String} - metrics name */ const buildCell = (branch, master, name) => { const branchMetric = getMetric(branch, name); const masterMetric = getMetric(master, name); const branchValue = branchMetric.value; const masterValue = masterMetric.value; const desiredLarger = branchMetric.desiredSize === 'larger'; const isChanged = branchValue !== masterValue; const larger = branchValue > masterValue; if (!isChanged) return `${branchValue}`; if (larger) return `${branchValue} ${desiredLarger ? '' : '' } **+${Math.abs(branchValue - masterValue).toFixed(2)}**`; return `${branchValue} ${!desiredLarger ? '' : '' } **-${Math.abs(branchValue - masterValue).toFixed(2)}**`; }; /** * Returns text of the comment with table inside * This table contain changes in all metrics * * @param branch {Object} report from feature branch * @param master {Object} report from master branch * @return {String} comment markdown */ const buildCommentText = (branch, master) =>{ const md = branch.map( page => { const pageAtMaster = getPage(master, page.subject); if (!pageAtMaster) return ''; const md = `|${page.subject}|${buildCell(page, pageAtMaster, 'Performance')}|${buildCell(page, pageAtMaster, 'Accessibility')}|${buildCell(page, pageAtMaster, 'Best Practices')}|${buildCell(page, pageAtMaster, 'SEO')}| `; return md; }).join(''); return ` |Path|Performance|Accessibility|Best Practices|SEO| |--- |--- |--- |--- |--- | ${md} `; };
将建立评论的脚本
您需要有一个令牌才能使用 GitLab API。 为了生成一个,您需要打开 GitLab,登录,打开菜单的“设置”选项,然后打开导航菜单左侧的“访问令牌”。 然后您应该能够看到允许您生成令牌的表单。

此外,您将需要项目的 ID。 您可以在存储库“设置”中找到它(在子菜单“常规”中):

要向合并请求添加注释,我们需要知道它的 ID。 允许您获取合并请求 ID 的函数如下所示:
// You can set environment variables via CI/CD UI. // @see https://gitlab.com/help/ci/variables/README#variables // I have set GITLAB_TOKEN this way // ID of the project const GITLAB_PROJECT_ID = process.env.CI_PROJECT_ID || '18090019'; // Token const TOKEN = process.env.GITLAB_TOKEN; /** * Returns iid of the merge request from feature branch to master * @param from {String} — name of the feature branch * @param to {String} — name of the master branch * @return {Number} — iid of the merge request */ const getMRID = async (from, to) => { const response = await fetch(`https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests?target_branch=${to}&source_branch=${from}`, { method: 'GET', headers: { 'PRIVATE-TOKEN': TOKEN, } }); if (!response.ok) throw new Error('Merge request not found'); const [{iid}] = await response.json(); return iid; };
We need to get a feature branch name. You may use the environment variable CI_COMMIT_REF_SLUG
inside the pipeline. Outside of the pipeline, you can use the current-git-branch
package. Also, you will need to form a message body.
Let's install the packages we need for this matter:
npm i -S current-git-branch form-data
And now, finally, function to add a comment:
const FormData = require('form-data'); const branchName = require('current-git-branch'); // Branch from which we are making merge request // In the pipeline we have environment variable `CI_COMMIT_REF_NAME`, // which contains name of this banch. Function `branchName` // will return something like «HEAD detached» message in the pipeline. // And name of the branch outside of pipeline const CURRENT_BRANCH = process.env.CI_COMMIT_REF_NAME || branchName(); // Merge request target branch, usually it's master const DEFAULT_BRANCH = process.env.CI_DEFAULT_BRANCH || 'master'; /** * Adding comment to merege request * @param md {String} — markdown text of the comment */ const addComment = async md => { const iid = await getMRID(CURRENT_BRANCH, DEFAULT_BRANCH); const commentPath = `https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests/${iid}/notes`; const body = new FormData(); body.append('body', md); await fetch(commentPath, { method: 'POST', headers: { 'PRIVATE-TOKEN': TOKEN, }, body, }); };
And now we can generate and add a comment:
cluster.on('message', (worker, msg) => { report = [...report, ...msg]; worker.disconnect(); reportsCount++; if (reportsCount === chunks.length) { fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + if (CURRENT_BRANCH === DEFAULT_BRANCH) process.exit(0); + try { + const masterReport = await getArtifact('performance.json'); + const md = buildCommentText(report, masterReport) + await addComment(md); + } catch (error) { + console.log(error); + } process.exit(0); } });
- Check the gist and demo repository.
Now create a merge request and you will get:

恢复
Comments are much less visible than widgets but it's still much better than nothing. This way we can visualize the performance even without artifacts.
验证
OK, but what about authentication? The performance of the pages that require authentication is also important. It's easy: we will simply log in. puppeteer
is essentially a fully-fledged browser and we can write scripts that mimic user actions:
const LOGIN_URL = '/login'; const USER_EMAIL = process.env.USER_EMAIL; const USER_PASSWORD = process.env.USER_PASSWORD; /** * Authentication sctipt * @param browser {Object} — browser instance */ const login = async browser => { const page = await browser.newPage(); page.setCacheEnabled(false); await page.goto(`${DOMAIN}${LOGIN_URL}`, { waitUntil: 'networkidle2' }); await page.click('input[name=email]'); await page.keyboard.type(USER_EMAIL); await page.click('input[name=password]'); await page.keyboard.type(USER_PASSWORD); await page.click('button[data-test]', { waitUntil: 'domcontentloaded' }); };
Before checking a page that requires authentication, we may just run this script. 完毕。
概括
In this way, I built the performance monitoring system at Werkspot — a company I currently work for. It's great when you have the opportunity to experiment with the bleeding edge technology.
Now you also know how to visualize performance change, and it's sure to help you better track performance degradation. 但接下来会发生什么? You can save the data and visualize it for a time period in order to better understand the big picture, and you can collect performance data directly from the users.
You may also check out a great talk on this subject: “Measuring Real User Performance In The Browser.” When you build the system that will collect performance data and visualize them, it will help to find your performance bottlenecks and resolve them. 祝你好运!