教你编写简易的性能检测工具

1,425 阅读7分钟

前言

性能优化一直是我们需要注意的问题,尤其是对于C端产品来说更是重中之重。而关于性能优化的文章教材层出不穷,这里不过多赘述。而对于性能优化过后,性能到底提升了多少,我们需要有一个很直观的数据,才能更加方便我们工作,也能更清楚的计算出我们所获得的成果。

效果展示:

源码地址: pmat

现有工具

现在的性能检测方式非常多

  • Chrome 自带的开发者工具:Performance
  • Lighthouse 开源工具
  • 原生 Performance API
  • 各种官方库、插件

这些工具都各有各的优点,但同样都有一定的局限性。比如 Lighthouse 可以用可视化各类指标来非常直观的看到各项检测数据,但是却无法完成一些特定的需求,比如有的项目需要登录才能看到具体的页面等等...

而我们需要做的是,结合他们的一些特点,整合成我们所需要的东西,而且对于甚至不了解原生 Performance API这块知识的同学来说,是个非常好的学习跟实践的机会。

准备知识

这里在开始做工具之前,需要了解原生 Performance API知识点。

这里直接推荐 2018你应该知道的Web性能信息采集指南, 虽然时间有些久了,但看完基本上对这块已经有了些基本了解,这里就不赘述了。

想要什么样的工具

这里表明下我的思路其实就是结合了社区大佬写的 per-moniteurhiper

  • per-moniteur: 项目内注入js, 利用 PerformanceObserver 监听页面性能,在控制台输出结果,主要监测 FCP, LCP等性能指标
  • hiper: 命令行工具,利用 puppeteer启动无头浏览器多次启动输入网址即可返回平均监测数据, 不过只计算了 performance.timing(已过时)的数据

所以我的想法就是将它两者结合起来,即只要输入网址和请求次数,即可测试到它的性能指标(包括基础的检测数据以及性能指标数据),也可根据puppeteer扩展缓存设置等配置。

开始

工具全部使用typescript开发,发布前转换成commonjs语法,这也没有什么特别,基本所有人都会。

○ 开始入口

class Pmat {
  // 命令入口,解析参数
  public cli: Cli;
  // puppeteer 启动无头浏览器
  public puppeteer: Puppeteer;
  // 性能检测对象数组
  public observer: Observer;

  constructor() {
    this.cli = new Cli();
    this.puppeteer = new Puppeteer();
    this.observer = new Observer();
  }

  async run() {
    // 获取命令返回的参数
    const options = await this.cli.monitor();
    // 初始化无头浏览器
    const puppeteer = await this.puppeteer.init(options);

    ......

    const { count, url } = options;
    const { page, browser } = puppeteer;

    // listr2 创建任务
    const task = new Listr([
      {
        title: 'start executing',
        task: async () => {
          // 根据输入的打开的页面次数,进行检测
          for (let i = 0; i < count; i += 1) {
            // 执行生命周期 beforeStart
            await this.observer.beforeStart();
            await page.goto(url, { waitUntil: 'load' });
            // 开始检测
            await this.observer.start();
          }
        },
      },
      {
        title: 'start calculating',
        task: async () => {
          // 根据检测的值进行计算,获取平均数
          await this.observer.calculate();
        },
      },
    ]);

    ......
  }
}

上面代码就是启动入口的操作,即根据命令的输入参数进行相应的操作。

这里需要特别注意的是 count 默认是3,因为要检测 TTI 使用了外部 js 库,在注入的时候花费了太多的时间,所以尽量不要设置太高,或者就选择不要检测 TTI 指标

○ 创建无头浏览器

import puppeteer from 'puppeteer';

import type { IPuppeteerOutput } from './interface';
import type { ICliOptions } from '../cli/interface';

class Puppeteer {
  async init(options?: ICliOptions): Promise<IPuppeteerOutput> {
  
    const browser = await puppeteer.launch({
      product: 'chrome',
    });
    const page = await browser.newPage();

    // 获取命令行参数
    const { cache, javascript, online, useragent, tti } = options;

    // 设置每个请求忽略缓存
    await page.setCacheEnabled(cache);
    // 是否启用js
    await page.setJavaScriptEnabled(javascript);
    // 是否启用离线模式
    await page.setOfflineMode(!online);

    if (useragent) {
      await page.setUserAgent(useragent);
    }

    return { page, browser, tti };
  }
}

export default Puppeteer;

这里就是简单的使用 puppteer 创建无头浏览器,并根据命令行输入的内容设置浏览器属性。更多api的使用可以参照 官方文档

计算性能

○ 计算 navigation

navigation 是指通过 Performance接口获取的关于浏览器文档事件的指标的方法和属性。具体方法是 performance.getEntriesByType('navigation'), 便可获得类似以下格式的数据:

而其中具体的计算方式可以参考:

通过以上知识,就可以很简单地计算出我们想要的数据,计算方式如下:

总时长:duration,
Redirect: redirectEnd - redirectStart,
AppCache: domainLookupStart - fetchStart,
DNS: domainLookupEnd - domainLookupStart,
TCP: connectEnd - connectStart,
First Byte time: responseStart - requestStart,
Download: responseEnd - responseStart,
白屏时间: domInteractive - fetchStart,
DOMReady: domContentLoadedEventEnd - fetchStart,
Load: domContentLoadedEventEnd - domContentLoadedEventStart

注意:因为工具是多次请求一个地址,所以需要计算所有数据总和的平均值

○ 计算性能指标

为了能够更具体的量化性能优化这一块,我们需要获取谷歌提出的这一系列性能指标,但由于谷歌一直在更新性能优化的指标,以下的思维导图算是目前最新的一些性能指标,其中一些性能指标的计算不一定完全准确,只供参考,更详细的内容可以看还在看那些老掉牙的性能优化文章么?这些最新性能指标了解下

而我们如何通过代码去计算这些性能指标呢,主要是通过 PerformanceObserver 来获取数据,可以把它看成是一个监听器用来收集所需要的性能指标,主要格式如下所示:

const perfObserver = new PerformanceObserver((entryList) => {
    // 信息处理
})

// 传入需要的 type
perfObserver.observe({ entryTypes: ['paint'] })

注意: 本来这性能指标的计算方法是相对比较简单的,但由于是使用 puppeteer 来进行测试,所以在开发过程中遇到了一些困难,具体的可以阅读 Web Performance Recipes With Puppeteer

FP & FCP

  • FP(First Paint),首次绘制,这个指标用于记录页面第一次绘制像素的时间。

  • FCP(First Contentful Paint),首次内容绘制,这个指标用于记录页面首次绘制文本、图片、非空白 Canvas 或 SVG 的时间。

page.evaluateOnNewDocument(getPaint);

function getPaint() {
  window.FP = 0;
  window.FCP = 0;

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const { startTime, name } = entry;
      if (name === 'first-contentful-paint') {
        window.FCP = startTime;
      } else {
        window.FP = startTime;
      }
    }
  });

  observer.observe({ entryTypes: ['paint'] });
}

LCP

  • LCP(Largest Contentful Paint),最大内容绘制,用于记录视窗内最大的元素绘制的时间
await page.evaluateOnNewDocument(calcLCP);
await page.goto(url, { waitUntil: 'load', timeout: 60000 });

let lcp = await page.evaluate(() => {
    return window.largestContentfulPaint;
});

function calcLCP() {
	window.largestContentfulPaint = 0;

    const observer = new PerformanceObserver((entryList) => {
        const entries = entryList.getEntries();
        const lastEntry = entries[entries.length - 1];
        window.largestContentfulPaint = lastEntry.renderTime || lastEntry.loadTime;
    });

    observer.observe({ type: 'largest-contentful-paint', buffered: true });

    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
            observer.takeRecords();
            observer.disconnect();
            console.log('LCP:', window.largestContentfulPaint);
        }
    });
}

CLS

  • CLS(Cumulative Layout Shift),累计位移偏移,记录了页面上非预期的位移波动。
window.CLS = 0;

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      window.CLS += entry.value;
    }
  }
});

observer.observe({ entryTypes: ['layout-shift'] });

TTI

  • TTI(Time to Interactive),首次可交互时间。通俗来讲就是页面从 FCP 到可以点击交互的时间,这个指标非常的重要,基本上就标志了页面的性能。而它的计算需要符合下面的几个条件:
  1. FCP 指标后开始计算
  2. 持续 5 秒内无长任务(执行时间超过 50 ms)且无两个以上正在进行中的 GET 请求
  3. 往前回溯至 5 秒前的最后一个长任务结束的时间

TTI的计算使用了 tti-polyfill

window.__tti = { e: [] };

const observer = new PerformanceObserver((list) => {
  const fcp = performance.getEntriesByName('first-contentful-paint')[0].startTime;
  const entries = list.getEntries();

  window.__tti.e = window.__tti.e.concat(entries);
})
observer.observe({ entryTypes: ['longtask'] });

...

await page.addScriptTag({ path: './node_modules/tti-polyfill/tti-polyfill.js' });

// Time to Interactive
TTI = await page.evaluate(() =>
  window.ttiPolyfill ? window.ttiPolyfill.getFirstConsistentlyInteractive() : -1,
);

由于此代码的计算时间特别长,所以建议关闭 TTI, 或者谨慎设置过高的count(默认为3)。

FID

  • FID(First Input Delay),首次输入延迟,记录在 FCP 和 TTI 之间用户首次与页面交互时响应的延迟。
window.FID = 0;
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    window.FID = entry.processingStart - entry.startTime;
  }
});

observer.observe({ type: 'first-input', buffered: true });

TBT

  • TBT(Total Blocking Time),阻塞总时间,记录在 FCP 到 TTI 之间所有长任务的阻塞时间总和。
window.TBT = 0;

const observer = new PerformanceObserver((list) => {
  const fcp = performance.getEntriesByName('first-contentful-paint')[0].startTime;
  const entries = list.getEntries();

  for (const entry of entries) {
    if (entry.name !== 'self' || entry.startTime < fcp) {
      return;
    }
    // long tasks mean time over 50ms
    const blockingTime = entry.duration - 50;
    if (blockingTime > 0) window.TBT += blockingTime;
  }
});

observer.observe({ entryTypes: ['longtask'] });

最后

平时空闲的时候写了这个工具,一是为了学习性能检测方面的知识点,二是为了在之后对项目进行性能优化的时候能够更直观的看到优化的幅度。有问题的地方欢迎指正。感谢。