专栏首页DevOps时代的专栏网站性能测试利器:Puppeteer

网站性能测试利器:Puppeteer

译者:CK星空,本文由 DevOps 时代高翻院翻译整理发布

网站性能测试从来没有像今天这么重要。测试的工具有Lighthouse,WebPagetest,PageSpeed Insights,或只是浏览器中的性能面板。在这篇文章中,我会利用Puppeteer进行网站自动化测试。

1、被测试的应用程序

2、Navigation Timing API

3、Chrome DevTools 性能时间轴面板-首次有意义绘图

4、自定义页面指标

5、从网络跟踪中提取数据

6、模拟低速网络并节制 CPU

7、控制浏览器缓存和 service worker 的重复访问

8、结果

1、被测试的应用程序

我选了Vue Hacker News 2.0作为测试,这是HNPWA其中的一个应用。我选择这个app是因为它有良好的性能测试实践。而且很容易克隆和在本地环境运行。

所有的例子都是在本地运行的,但如果你不想这么做的话,你还可以使用live demo,网址是https://vue-hn.now.sh.简单地用我的例子http:// localhost:8080替换为https://vue-hn.now.sh。

但是,如果你使用live demo,则无法测量自定义页面指标,因为它需要在源代码中插入console.timeStamp()

2、Navigation Timing API

一开始,我们要测量网页加载速度。输出的数据应该和在浏览器控制台运行window.performance.timing 相同。

index.js

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('http://localhost:8080');

    const performanceTiming = JSON.parse(
        await page.evaluate(() => JSON.stringify(window.peformance.timing))
    );
    console.log(performanceTiming);

    await browser.close();
})();

上面的代码涵盖了所有”Hello World”的需要。puppeteer.launch()在无头模式下创建新的浏览器实例,接下来的browser.newPage()可以通过创建新的标签来识别。page.goto('http://localhost:8080')将一直等到事件加载发生或在30秒内发生不好的情况。

整个测试归结为page.evaluate()在page上下文中发送window.performance.timing,并使用JSON.parse()解码结果。

经过所有的页面测试,browser.close()就能简单地关闭浏览器,它也会删除所有的cache/service workers,因为我们没有传递userDataDir参数给puppeteer.launch()

运行node index.js之后,你将看到如下所示的原始页面加载数据:

{
  navigationStart: 1513433544980,
  unloadEventStart: 0,
  unloadEventEnd: 0,
  redirectStart: 1513433544980,
  redirectEnd: 1513433545292,
  fetchStart: 1513433545292,
  domainLookupStart: 1513433545292,
  domainLookupEnd: 1513433545292,
  connectStart: 1513433545292,
  connectEnd: 1513433545292,
  secureConnectionStart: 0,
  requestStart: 1513433545019,
  responseStart: 1513433545289,
  responseEnd: 1513433545292,
  domLoading: 1513433545296,
  domInteractive: 1513433545339,
  domContentLoadedEventStart: 1513433545540,
  domContentLoadedEventEnd: 1513433545540,
  domComplete: 1513433545602,
  loadEventStart: 1513433545602,
  loadEventEnd: 1513433545602,
}

这个结果没有告诉你有用的信息?我也是。正如你所看到的那些点是在某个任意时间点是准时的。我们应该计算每个点的差异和navigationStart时间。并不是所有的点对我们都有用,我们可以过滤掉一些不相关的。另外,现在是重构的时候了。

index.js

const puppeteer = require('puppeteer');
const testPage = require('./testPage');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  console.log(await testPage(page));
  await browser.close();
})();

testPage.js

const { extractDataFromPerformanceTiming } = require('./helpers');

async function testPage(page) {
  await page.goto('http://localhost:8080');

  const performanceTiming = JSON.parse(
    await page.evaluate(() => JSON.stringify(window.performance.timing))
  );

  return extractDataFromPerformanceTiming(
    performanceTiming,
    'responseEnd',
    'domInteractive',
    'domContentLoadedEventEnd',
    'loadEventEnd'
  );
}

module.exports = testPage;

helpers.js

const extractDataFromPerformanceTiming = (timing, ...dataNames) => {
  const navigationStart = timing.navigationStart;

  const extractedData = {};
  dataNames.forEach(name => {
    extractedData[name] = timing[name] - navigationStart;
  });

  return extractedData;
};

module.exports = {
  extractDataFromPerformanceTiming,
};

index.js包含特定的浏览器启动代码,testPage.js只关注正在运行的测试,而helpers.js具有用于解析的特定的函数和转换结果。

现在结果被很好地解析并以毫秒表示:

{ // all results are in [ms]
  responseEnd: 23,
  domInteractive: 44,
  domContentLoadedEventEnd: 196,
  loadEventEnd: 241
}

3、Chrome DevTools性能时间轴面板-首次有意义绘图

本章将使用Chrome性能指标(Chrome Performance Metrics)。但是,在上一章中我们不是已经测试性能计时了吗?是的,你可能会感到困惑。window.performance.timing是由W3C维护的浏览器不可知测量标准(agnostic measure standard),所有的浏览器都应该有相同的API。

另一方面,本章中的“性能指标”是基于Chrome浏览器的特定指标(如性能面板),它们不仅有计时,还包含一些其他指标,如:

[
  { name: 'Timestamp', value: 35037.202627 },
  { name: 'AudioHandlers', value: 0 },
  { name: 'Documents', value: 3 },
  { name: 'Frames', value: 2 },
  { name: 'JSEventListeners', value: 63 },
  { name: 'LayoutObjects', value: 435 },
  { name: 'MediaKeySessions', value: 0 },
  { name: 'MediaKeys', value: 0 },
  { name: 'Nodes', value: 506 },
  { name: 'Resources', value: 11 },
  { name: 'ScriptPromises', value: 0 },
  { name: 'PausableObjects', value: 39 },
  { name: 'V8PerContextDatas', value: 1 },
  { name: 'WorkerGlobalScopes', value: 1 },
  { name: 'UACSSResources', value: 0 },
  { name: 'LayoutCount', value: 2 },
  { name: 'RecalcStyleCount', value: 5 },
  { name: 'LayoutDuration', value: 0.0860430000029737 },
  { name: 'RecalcStyleDuration', value: 0.00374899999587797 },
  { name: 'ScriptDuration', value: 0.0770069999925909 },
  { name: 'TaskDuration', value: 0.297364000020025 },
  { name: 'JSHeapUsedSize', value: 6295344 },
  { name: 'JSHeapTotalSize', value: 10891264 },
  { name: 'FirstMeaningfulPaint', value: 35036.03356 },
  { name: 'DomContentLoaded', value: 35036.122972 },
  { name: 'NavigationStart', value: 35035.833805 },
]

现在是时候解释一下为什么Puppeteer是比Chrome DevTools协议更高级的API。Puppeteer真的有助于普通的测试任务(如点击元素和填充输入等)。但有些功能你能用原始的Chrome DevTools 协议实现,而Puppeteer API不能。

目前,在0.13版本中,只有通过page._client.send()才能获得原始协议的方法。在不久的将来会改变的。我们将通过page._client.send('Performance.getMetrics')来发送,使用来自原始的DevTools协议的方法getMetrics。

testPage.js

const {
  getTimeFromPerformanceMetrics,
  extractDataFromPerformanceMetrics,
} = require('./helpers');

async function testPage(page) {
  await page.goto('http://localhost:8080');

  await page.waitFor(1000);
  const performanceMetrics = await page._client.send('Performance.getMetrics');

  return extractDataFromPerformanceMetrics(
    performanceMetrics,
    'FirstMeaningfulPaint'
  );
}

module.exports = testPage;

helpers.js

const getTimeFromPerformanceMetrics = (metrics, name) =>
  metrics.metrics.find(x => x.name === name).value * 1000;

const extractDataFromPerformanceMetrics = (metrics, ...dataNames) => {
  const navigationStart = getTimeFromPerformanceMetrics(
    metrics,
    'NavigationStart'
  );

  const extractedData = {};
  dataNames.forEach(name => {
    extractedData[name] =
      getTimeFromPerformanceMetrics(metrics, name) - navigationStart;
  });

  return extractedData;
};

module.exports = {
  getTimeFromPerformanceMetrics,
  extractDataFromPerformanceMetrics,
};

这个代码与前一章的代码类似,但是请记住window.performance.timing在网页上下文中执行,Performance.getMetrics在浏览器级别上执行(特别是Chrome)。这就是为什么两个指标的navigationStart时间都不相同。

如果你在testPage.js中发现了奇怪的代码page.waitFor(1000),这就对了。但为什么需要延迟测量首次有意义绘图?在这个例子中首次有意义绘图小于加载事件时间,你可能会更困惑(并await page.goto('http:// localhost:8080')直到load事件。这是由于首次有意义绘图不是准时的任意时间点,这个测量是基于一些启发式的,并且是在所有页面渲染完毕后计算的。

首次有意义绘制没有在合适的场景下准备,所以我们不能精确地检测这个标准是什么时候完成的。但是,如果度量标准已准备就绪,我们可以制定一个解决方法来检查每个时间点:

testPage.js

async function testPage(page) {
  // ...

  // await page.waitFor(1000);
  // const performanceMetrics = await page._client.send('Performance.getMetrics');

  let firstMeaningfulPaint = 0;
  let performanceMetrics;
  while (firstMeaningfulPaint === 0) {
    await page.waitFor(300);
    performanceMetrics = await page._client.send('Performance.getMetrics');
    firstMeaningfulPaint = getTimeFromPerformanceMetrics(
      performanceMetrics,
      'FirstMeaningfulPaint'
    );
  }

  // ...
}

现在,当代码没有竞争时(the code is free of race conditions),可以显示示例结果:

{ // result is in [ms]
  FirstMeaningfulPaint: 175
}

4、自定义页面指标

前面的章节涵盖了可用于所有网站的指标 - 它们是通用的。现在我们将尝试衡量一些app-specific的指标。我选择一个例子来说明,分页导航按钮"<prev""more>" 是由JavaScript控制。为什么这个点准时是重要的?因为这个app在客户端使用hydration markup 的SSR。

这种方法导致“不可思议的谷”,FirstMeaningfulPaint可以被认定是“不可思议的谷”的开始,并且我们的自定义标准listLinksSpa将代表“不可思议的谷”结束的时间。 我们必须判断在哪里写入console.timeStamp('listLinksSpa')。我推断,mounted()方法放在ItemList.vue是一个好的做法:

src/views/ItemList.vue

beforeMount() { /* ... */ },

mounted() {
  console.timeStamp('listLinksSpa');
},

beforeDestroy() { /* ... */ },

现在我们必须在page.goto()之前注册监听器page.on('metrics',callback)。当我们的应用程序调用page.on('metrics',callback)中的console.timeStamp('listLinksSpa')回调函数时,我们可以提取这个度量的时间。

testPage.js

const { getTimeFromPerformanceMetrics } = require('./helpers');

async function testPage(page) {
  let listLinksSpa;
  page.on('metrics', ({ title, metrics }) => {
    if (title === 'listLinksSpa') {
      listLinksSpa = metrics.Timestamp * 1000;
    }
  });

  await page.goto('http://localhost:8080');

  const performanceMetrics = await page._client.send('Performance.getMetrics');
  const navigationStart = getTimeFromPerformanceMetrics(
    performanceMetrics,
    'NavigationStart'
  );
  await page.waitFor(1000);

  return {
    listLinksSpa: listLinksSpa - navigationStart,
  };
}

module.exports = testPage;

上面的代码应该没问题。但是代码质量与此await page.waitFor(1000)是远远不能接受的 - 竞争条件的脆弱性在这里太明显了(the vulnerability to race condition is too obvious here)。我举上面的例子只是为了引出一个简单的例子。下面的代码通过在一个promise中包含page.on(’metrics’,callback)来解决这个问题,并使用了async/await的特性 。

testPage.js

const { getTimeFromPerformanceMetrics, getCustomMetric } = require('./helpers');

async function testPage(page) {
  const listLinksSpa = getCustomMetric(page, 'listLinksSpa');

  await page.goto('http://localhost:8080');

  const performanceMetrics = await page._client.send('Performance.getMetrics');
  const navigationStart = getTimeFromPerformanceMetrics(
    performanceMetrics,
    'NavigationStart'
  );

  return {
    listLinksSpa: (await listLinksSpa) - navigationStart,
  };
}
module.exports = testPage;

helpers.js

const getTimeFromPerformanceMetrics = (metrics, name) =>
  metrics.metrics.find(x => x.name === name).value * 1000;

const getCustomMetric = (page, name) =>
  new Promise(resolve =>
    page.on('metrics', ({ title, metrics }) => {
      if (title === name) {
        resolve(metrics.Timestamp * 1000);
      }
    })
  );

module.exports = {
  getTimeFromPerformanceMetrics,
  getCustomMetric,
};

现在结果应该是这样的:

{ // result is in [ms]
  listLinksSpa: 230
}

5、从网络跟踪面板中提取数据

在Chrome面板中运行性能测试时,可以将数据保存为JSON文件。在Puppeteer中也是一样。只要在page.goto()之前用page.tracing.start({path:'./trace.json'})开始记录跟踪,并且当你认为你需要的所有东西都被记录时,用page.tracing.stop()停止记录。

在下面的代码中,我只展示提取CSS文件的开始和结束网络请求时间。

testPage.js

const {
  getTimeFromPerformanceMetrics,
  extractDataFromTracing,
} = require('./helpers');

async function testPage(page) {
  await page.tracing.start({ path: './trace.json' });

  await page.goto('http://localhost:8080');

  await page.tracing.stop();
  const cssTracing = await extractDataFromTracing(
    './trace.json',
    'common.3a2d55439989ceade22e.css'
  );

  const performanceMetrics = await page._client.send('Performance.getMetrics');
  const navigationStart = getTimeFromPerformanceMetrics(
    performanceMetrics,
    'NavigationStart'
  );

  return {
    cssStart: cssTracing.start - navigationStart,
    cssEnd: cssTracing.end - navigationStart,
  };
}

module.exports = testPage;

trace.json真的是一个信息地雷,对于这个简单的app大小达到683 KB。对于典型的网站,它可以达到几MB。这个文件中的数据是相当原始的,你应该准备深入挖掘里面的信息。

在代码中,getTimeFromPerformanceMetrics()ResourceSendRequest跟踪类型中搜索请求的文件。当它发现它会得到它的开始时间和resourceIdresourceId用于在资源结束时查找记录。

helpers.js

const fs = require('fs');

const getTimeFromPerformanceMetrics = (metrics, name) =>
  metrics.metrics.find(x => x.name === name).value * 1000;

const extractDataFromTracing = (path, name) =>
  new Promise(resolve => {
    fs.readFile(path, (err, data) => {
      const tracing = JSON.parse(data);

      const resourceTracings = tracing.traceEvents.filter(
        x =>
          x.cat === 'devtools.timeline' &&
          typeof x.args.data !== 'undefined' &&
          typeof x.args.data.url !== 'undefined' &&
          x.args.data.url.endsWith(name)
      );
      const resourceTracingSendRequest = resourceTracings.find(
        x => x.name === 'ResourceSendRequest'
      );
      const resourceId = resourceTracingSendRequest.args.data.requestId;
      const resourceTracingEnd = tracing.traceEvents.filter(
        x =>
          x.cat === 'devtools.timeline' &&
          typeof x.args.data !== 'undefined' &&
          typeof x.args.data.requestId !== 'undefined' &&
          x.args.data.requestId === resourceId
      );
      const resourceTracingStartTime = resourceTracingSendRequest.ts / 1000;
      const resourceTracingEndTime =
        resourceTracingEnd.find(x => x.name === 'ResourceFinish').ts / 1000;

      resolve({
        start: resourceTracingStartTime,
        end: resourceTracingEndTime,
      });
    });
  });

module.exports = {
  getTimeFromPerformanceMetrics,
  extractDataFromTracing,
};

CSS文件的跟踪数据如下所示。请记住,这只是数据中一千条记录中的一条。

{ // all results are in [ms]
  cssStart: 27,
  cssEnd: 40
}

6、模拟低速网络和throttle CPU

以上所有的结果不可思议的快。这是因为我使用本地主机上的高端设备运行所有测试。真实的用户的网络连接一般较弱,他们的计算能力没那么强大。我们可以使用Network.emulateNetworkConditionsEmulation.setCPUThrottlingRate轻松地模拟这种情况。而且,设置固定的网络条件有助于测试的可重复性。这一个CPU节流器只是相对延缓你的CPU(在不同的机器你会得到不同的结果)。

index.js

const puppeteer = require('puppeteer');
const testPage = require('./testPage');

(async () => {
  let browser = await puppeteer.launch();
  let page = await browser.newPage();
  await page._client.send('Network.emulateNetworkConditions', { // 3G Slow
    offline: false,
    latency: 200, // ms
    downloadThroughput: 780 * 1024 / 8, // 780 kb/s
    uploadThroughput: 330 * 1024 / 8, // 330 kb/s
  });
  await page._client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
  console.log(await testPage(page));
  await browser.close();
})();

现在你可以看到以前章节的汇总结果更加符合实际情况。

{ // all results are in [ms]
  cssStart: 542,
  cssEnd: 789,
  listLinksSpa: 2322,
  FirstMeaningfulPaint: 1061,
  responseEnd: 517,
  domInteractive: 812,
  domContentLoadedEventEnd: 2111,
  loadEventEnd: 2336
}

7、控制浏览器缓存和service worker的重复访问

我们没有测试service worker对性能的影响,因为我们的测试总是使用纯净的浏览器实例。要测量网页将如何与缓存或service worker呈现,我们必须在同一个浏览器实例中第二次运行我们的测试。之后,当我们调用browser.close()时,所有的缓存数据和service worker都将被清除,因为我们没有在puppeteer.launch()中指定任何userDataDir

如果我们想测试重复访问只有缓存没有service worker,我们必须停止service worker在前一个入口注册通过ServiceWorker.stopAllWorkers之间的第一个结束第二个输入。 如果我们只想单独测试service worker,Network.clearBrowserCache也是如此。

注意从其余的例子page._client.send('ServiceWorker.enable')。 Chrome DevTools协议需要启用特定域名,但其中一些域名是由Puppeteer启用的。 ServiceWorker域名不在Puppeteer中使用,所以我们必须手工启动它。

index.js

const puppeteer = require('puppeteer');
const testPage = require('./testPage');

(async () => {
  let browser = await puppeteer.launch();
  let page = await browser.newPage();
  console.log(await testPage(page)); // first enter
  console.log(await testPage(page)); // second enter with cache and sw
  await browser.close();

  browser = await puppeteer.launch();
  page = await browser.newPage();
  await testPage(page); // only for creating fresh instance
  await page._client.send('ServiceWorker.enable');
  await page._client.send('ServiceWorker.stopAllWorkers');
  console.log(await testPage(page)); // second enter only with cache
  await browser.close();

  browser = await puppeteer.launch();
  page = await browser.newPage();
  await testPage(page); // only for creating fresh instance
  await page._client.send('Network.clearBrowserCache');
  console.log(await testPage(page)); // second enter only with sw
  await browser.close();
})();

8、结果

你可以用各种原因来分析这些数据。 研究新功能对性能变化的影响,观察持续集成中的某些性能下降,简单地展示一些像我将要做的奇特的功能。

对于每个plot我运行测试100次,600页的入口,大约需要10 - 20分钟,每个测试套件。

有线网络

对于普通用户来说,最重要的指标是FirstMeaningfulPaintlistLinksSpa。这些指标反映了他的网站速度。domInteractive与网站对用户交互的时间没有任何关系,这个度量在本例中由一个自定义listLinksSpa表示。

responseEnd是显示网络带宽和延迟对页面的影响的一个很好的指标。 第二次只进入高速缓存,通常状态为304,并且其服务速度不会超过双倍延迟时间-这就是为什么来自高速缓存的responseEnd发生在60-70毫秒左右的原因。另一方面,responseEnd,service worker省略了网络层,并且不受延迟的影响。 service worker 提供服务时,只有设备处理能力(CPU)会影响此度量标准。

只有service worker(sw)和有缓存的service worker之间没有统计上的显着差异,这是因为app中的所有网络请求都被service worker覆盖。例如,如果有一些不是由service worker处理的图片,而只是通过传统的缓存,我们将看到service worker和缓存相结合的好处。

配置service worker 需要一些工作,但是如果仅仅关注良好的网络和好设备的结果,益处并不是那么明显。 如果网络速度下降,一切都会改变。

好设备,慢3G网络

由service worker处理的度量标准的时间与上图中的相同。 由于双重延迟,仅从缓存中提供的请求浪费了大量时间。 这就是为什么大延迟是移动网络中最有问题的因素,而不是小带宽。

总体来看,这一点很明显,这就是为什么service worker会应用到移动设备。 你可以使用service worker提高编程的网站速度,可以提高网络带宽,但不能极大地提高速度。

慢3G网,差设备

受影响最严重的service worker的结果是减少6倍的CPU性能。这个图用loadEventEnd和上一个进行比较:

首次进入速度降低1.4倍 只有缓存的速度要慢2.6倍 service worker慢5.8倍

Service worker看起来没有像传统的缓存优化的性能那么高 - 但仍然是一项新技术。

不管你想要研究什么,我希望我已经帮助了你如何用Puppeteer获得结果。这个工具很容易安装。

只要输入npm install puppeteer

本文分享自微信公众号 - DevOps时代(DevOpsTimes)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-02-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • DevOps前世今生 | 2. Dev和Ops矛盾缘何而来?

    ? 一、前言 在《DevOps的前世今生 | 1. DevOps编年史》一文中,通过追溯 DevOps 活动产生的历史起源,我们发现了 DevOps 是敏捷思...

    DevOps时代
  • DevOps 的前世今生:Dev 和 Ops 矛盾缘何而来?

    前言 在「DevOps 前世今生之 DevOps 编年史」一文中,通过追溯 DevOps 活动产生的历史起源,我们发现了 DevOps 是敏捷思想从软件开发端(...

    DevOps时代
  • 特性分支与特性开关哪家强?

    分支管理策略对一个研发团队发布高质量的软件至关重要。在本文中,我们将探讨同一代码库中多任务并行开发时的解决方案,以及它们之间的优缺点。一般意义上来说冲突合并成本...

    DevOps时代
  • Puppeteer已经取代PhantomJs

    记得前几年,我们通常会用PhantomJs做一下自动化测试,或者为了SEO优化,会用它对SPA页面进行预渲染,现在有更好的Puppeteer来代替它的工作了,性...

    javascript.shop
  • mysql创建数据表时如何判断是否已经存在?

    >>> create table if not exists people(name text,age int(2),gender char(1)); 如上代...

    marsggbo
  • cisoco 与 H3C查ARP

    Apr 18 10:24:16.265: %IP-4-DUPADDR: Duplicate address 172.30.30.62 on Vlan711, s...

    py3study
  • HTTP和RPC的优缺点

    在HTTP和RPC的选择上,可能有些人是迷惑的,主要是因为,有些RPC框架配置复杂,如果走HTTP也能完成同样的功能,那么为什么要选择RPC,而不是更容易上手的...

    JouyPub
  • iOS开发中解决报错之the file had a tree conflict

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

    用户1451823
  • 小程序——移动零售电商“霸主”

    移动互联网领域的竞争的生存机率是七万分之一零售电商该如何提高销量,并提升自身竞争力

    快销手公众号小程序开发
  • 猿蜕变18——一文掌控SSM玩耍方式

    看过之前的蜕变系列文章,相信你对SpringMVC 、Spring、 Mybatis的整合有了一定的心得,学会了搭建属于自己的开发框架。今天我们就在这个基础上写...

    山旮旯的胖子

扫码关注云+社区

领取腾讯云代金券