从一个接口请求全过程分析网页性能指标

前言

最近工作中遇到一个需求:输入任意系统内的某个页面地址,罗列出尽可能多的页面性能指标数据,并且想办法收集起来。

提到性能指标,第一时间脑海里浮现的是什么 FCP,LCP,CLS 这些字眼,但仅仅只罗列出这几个数据,未免难以交差,并且还得想办法把指标数据收集起来。在参阅资料后,终于有了比较清晰的一个思路,下面我以一个接口请求所要经历的全流程为切入点,分析都有哪些性能指标可以使用并且如何获取。

接口请求过程

日常工作中,每天都会跟接口打交道。不过往往我们所关注的,是接口返回的数据。不过既然要罗列性能指标数据,那肯定不能对接口请求流程停留在"建立TCP连接,服务器处理请求,浏览器接收数据,断开TCP连接"这个泛泛而谈的过程,而是要更深入一些,了解更多的步骤,才能从中找到我们想要的东西。

重定向

当浏览器向服务器请求资源,服务器告知需要重定向时,就会存在重定向的耗时。例如:服务器返回301状态码,说明资源永久重定向,即浏览器下次再访问该资源时,会直接到重定向的地址去获取。而如果服务器返回302状态码,说明资源临时重定向,即浏览器下次再访问该资源时,还是会先访问服务器,然后再访问服务器给出的重定向地址去获取。

构建请求

我们要请求接口获取资源,那么浏览器得知道需要通过什么方式什么路径以及什么协议去获取资源,所以第一步需要构建请求,包括构建请求行和请求头,如果是POST请求的话还需要构建请求体,然后才能进行下一步。请求头和请求体我们都比较熟悉,打开network面板即可看到接口的请求头和请求体:

至于请求行,可能不太熟悉,其实就是标记了请求方式,资源路径以及协议这些信息,像这样:

查看缓存

构建完了请求,浏览器知道需要获取的资源是什么了,是不是就立即开始发送请求了呢?不是的 ,因为有缓存这个概念,所以第二步会从缓存中查找所需资源是否已存在,这里涉及到常常谈论到的"强缓存"和"协商缓存"等概念,不过不是本文的重点,就不展开了。如果缓存里有所需的资源,浏览器直接使用,不会发起请求。如果没有,那就下一步。

DNS解析

好了,经过上一步查找缓存,发现没有找到目标资源,这时就得发起请求向服务器获取资源了。那是不是立马开始建立TCP连接呢?当然不是,因为要建立连接,我们首先得知道要向哪一个服务器获取,所以得知道服务器的信息,即 IP。所以这一步的操作是DNS解析,即获取服务器的IP,涉及到根据域名解析IP地址的DNS查询概念,例如:"迭代查询"、"递归查询"啥的。

TCP队列等待

经过DNS解析,我们知道了要向哪个服务器获取资源,但此时也未必能开始发送请求。我们知道,如果使用的是Http1.1,那么可能会存在队头阻塞的情况,即浏览器最多同时与服务器维持6个TCP连接,如果这些连接都仍在处理上一个请求,那么当前请求就得进入请求队列进行等待。

建立TCP连接

如果不存在队头阻塞的情况,或者使用的是Http2协议,就不会有应用层面的队头阻塞了,那此时就开始与服务器建立TCP连接了。浏览器与服务器经过TCP三次握手后建立连接,之后两者通过连接交换数据。

建立TLS连接

如果使用的是Https协议,那么还需要进行用于保证数据安全的加密连接的建立。

发送请求

TCP连接建立好后,浏览器和服务器之间就可以通信了,此时浏览器可发送请求给服务器处理。

服务器处理请求

服务器接收到来自浏览器的请求后,就着手进行数据的获取,从数据库啥的获取资源。

服务器响应数据

服务器拿到数据后,就把数据通过TCP连接发送给浏览器。

断开TCP连接

当浏览器接收到返回的资源,那么就可以断开与服务器的连接了,即四次挥手断开连接。

请求全流程小结

文字描述不够直观,以一个图来串联起上述说到的过程:

性能指标获取

说了这么多,那么从上述这些步骤中,我们能罗列出什么性能指标数据呢。接下来,就要介绍到需要使用到的API了。

performance API

在浏览器中,有名为 performance 的API,我们可以借助它来获取页面的性能指标:

genEntries

通过 performance.genEntries,我们可以获取到关于页面的不同类型性能指标数据:

展开某个元素看看里面都有啥字段:

可以看到里面有一堆的字段,看得人眼花缭乱。不过,我们看到有几个字段名有点亲切:redirectStartredirectEndrequestStartresponseStartresponseEnd。直觉上看,这不就是重定向开始/结束时间戳,请求发起,请求响应开始/结束时间戳么。是的,没错,里面的字段对应上面说到的整个流程:

上图只是把部分字段整合到了流程图里,各字段的详细解释请戳这里:PerformanceNavigationTiming 。链接里还展示了PerformanceNavigationTiming 中定义的所有字段(时间戳属性):

genEntriesByType

上面提到的 PerformanceNavigationTiming,是记录页面整个导航过程中各阶段指标的数据结构。而除了页面导航过程,页面还需要请求其所需的各类资源,他们的指标数据结构名为 PerformanceResourceTiming ,其内容字段和 PerformanceNavigationTiming 基本一致,我们可以利用 performance.getEntriesByType('resource') 获取页面所有的资源指标情况:

利用performance.getEntriesByType('navigation') 获取页面导航过程中的情况:

性能指标定义

自定义性能指标

上面提到的单个字段,例如: redirectStartredirectEndrequestStartresponseStartresponseEnd。单个使用没啥用处,因为他们都是记录某个指标的时间戳,还是得通过计算不同字段之间的时间段来获取我们需要的指标。

页面重定向耗时

很明显,要获取页面的重定向时间,我们只需通过 performance.getEntriesByType('navigation') 获取页面的导航信息,然后计算 redirectEnd - redirectStart 即可。

计算规则: redirectEnd - redirectStart

DNS寻址耗时

计算规则:domainLookupEnd - domainLookupStart

TCP连接耗时

计算规则:connectEnd - connectStart

TLS连接耗时

计算规则:connectEnd - secureConnectionStart

资源请求总耗时

计算规则:responseEnd - requestStart

资源下载耗时/资源响应耗时

从服务器发送首个响应报文开始直到最后一个响应报文发送完毕,就是服务器的响应耗时,当然也就是资源下载的耗时。

计算规则:responseEnd - reponseStart

资源获取总耗时

上面的"资源请求总耗时"只是计算从请求发起到响应结束这段时间,但其实整个过程不止这个时间段,我们可以把从 startTimeresponseEnd 这段时间作为资源获取总耗时。当然了这个只是约定而已,看具体业务场景自行灵活选取哪两个字段间的时间间隔即可

计算规则:responseEnd - startTime

Dom树解析耗时

计算规则:domComplete - responseEnd

常见核心性能指标

这里列出几个使用比较普遍的页面性能指标

FCP

FCP(First Contentful Paint),首次内容绘制。

1)描述:指的是页面从加载开始直到页面任意内容渲染完成的耗时。值为浮点数,单位为ms。

首次内容绘制 (FCP) 是一项以用户为中心的重要指标,用于衡量感知的加载速度。它标记了网页加载时间轴中用户可以看到屏幕上任何内容的第一个点。快速的 FCP 有助于让用户确信正在发生的事情。

FCP 有助于让用户确信正在发生的事情。 FCP 衡量的是从用户首次导航到相应网页到该网页的任何部分呈现在屏幕上所用的时间。对于此指标,"内容"是指文本、图片(包括背景图片)、 元素或非白色 元素。

图片来源(侵删):First Contentful Paint (FCP)

2)触发时机:页面加载过程中自动触发

3) 指标衡量:值越小代表性能越好。说明页面首次内容渲染越快,具体判别标准见下图:

图片来源(侵删):First Contentful Paint (FCP)

LCP

LCP(Largest Contentful Paint),最大内容绘制。

1)描述:从页面加载开始直到视口中尺寸最大的内容渲染完成的耗时。值为浮点数,单位为ms。值得注意的是,因为随着页面的渲染,视口内的最大内容可能会发生变化,所以LCP指标可能会发生多次。

LCP 报告的是视口中可见最大图片或文本块的呈现时间(相对于用户首次导航到相应网页的时间)。
Largest Contentful Paint 考虑的元素类型包括:

  • <img> 元素(第一帧呈现时间用于 GIF 或动画 PNG 等动画内容)
  • <svg> 元素内的 <image> 元素
  • <video> 元素(系统会使用视频的海报图片加载时间或第一帧显示时间,以较早者为准)
  • 一个元素,带有使用 url() 函数(而不是 CSS 渐变)加载的背景图片
  • 包含文本节点或其他内嵌级文本元素子元素的块级元素。

将元素限制为这个有限的集合是有意降低复杂性的。随着开展更多研究,未来可能会添加其他元素(例如完整的 <svg> 支持)。

图片来源(侵删):Largest Contentful Paint (LCP)

2)触发时机:页面加载过程中自动触发。 注意:后台加载的页面不会触发

3)指标衡量:值越小代表性能越好。具体标准判别见下图:

图片来源(侵删):Largest Contentful Paint (LCP)

FID

FID(First Input Delay),首次互动延迟。值为浮点数,单位为ms。

1)描述:从用户首次与浏览器互动到浏览器做出响应的耗时。

FID 衡量的是从用户首次与网页互动(即,点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器实际能够开始处理事件处理脚本以响应相应互动的时间。

2)触发时机:用户首次与浏览器互动,如:点击页面或页面内链接等。

3)指标衡量:值越小代表性能越好,响应越迅速。具体标准见下图:

图片来源(侵删):First Input Delay (FID)

TTFB

TTFB(Time To First Byte),首次字节加载时间。值为浮点数,单位为ms。

1)描述:从页面加载开始直到浏览器接收到首个响应字节的耗时。

首字节时间 (TTFB) 是一项基本指标,用于在实验室和现场衡量连接设置时间和网络服务器响应能力。它测量的是从请求资源到响应的第一个字节开始到达所经过的时间。这有助于识别 Web 服务器何时因速度过慢而无法响应请求。对于导航请求(即对 HTML 文档的请求),它先于其他有意义的加载性能指标。
TTFB 是以下请求阶段的总和:

  • 重定向时间
  • Service Worker 启动时间(如果适用)
  • DNS 查找
  • 连接和 TLS 协商
  • 请求,直到响应的第一个字节到达

缩短连接设置时间和后端的延迟时间有助于降低 TTFB。

2)触发时机:页面加载过程中自动触发。

3)指标衡量:值越小代表性能越好。具体标准见下图:

图片来源(侵删):Time To First Byte(TTFB)

CLS

CLS(Cumulative Layout Shift),累计布局偏移。值为浮点数,没有单位。

1)描述:页面生命周期内每次布局意外偏移的最大比例得分。

CLS 衡量的是网页生命周期内发生的每次意外布局偏移的最大布局偏移得分。
Cumulative Layout Shift (CLS) 是一项稳定的 Core Web Vitals 指标。这是以用户为中心的一项重要指标,用于衡量视觉稳定性,因为它有助于量化用户遇到意外布局偏移的频率。较低的 CLS 有助于确保网页具有令人愉悦的体验。

意外的布局偏移可能会在很多方面干扰用户体验,包括在文本突然移动导致用户在阅读时丢失位置,以及让用户点击错误的链接或按钮。在某些情况下,这可能会造成严重损害。

下面动图为例,演示页面布局的意外偏移,导致误点击了蓝色确认按钮:

动图来源(侵删):Cumulative Layout Shift (CLS)

2)触发时机:页面生命周期内的每次页面渲染都有可能发生。实际测试中,发现在页面渲染完成后,将页面切后台再切回来,会触发。需要注意的是,如果页面加载全程都是在后台,则不会触发

3)指标衡量:值越小代表布局偏移比例越小,用户误操作概率越低。具体标准见下图:

图片来源(侵删):Cumulative Layout Shift (CLS)

获取网页性能指标数据

普通方式获取

获取上面提到的性能指标数据,我们可以使用提到的 performance 相关API来获取,即 getEntriesByType 。至于 FCP, LCP, FID, TTFB, CLS 指标的获取,也有对应的 PerformanceObserver API 来获取:

js 复制代码
// 获取 FCP 指标
const getFCP = () => {
    new PerformanceObserver((entries) => {
        const list = entries.getEntriesByName('first-contentful-paint');
        console.log('[FCP] list ==> ', list);
        list.forEach(entry => console.log('[FCP] value: ', entry.startTime));
    }).observe({ type: 'paint', buffered: true });
};
    
// 获取 LCP 指标
const getLCP = () => {
    new PerformanceObserver((enteries) => {
        const list = enteries.getEntries();
        console.log('[LCP] list ==> ', list);
        list.forEach(entry => console.log('[LCP] value: ', entry.startTime));
    }).observe({ type: 'largest-contentful-paint', buffered: true });
};
    
// 获取 FID 指标
const getFID = () => {
    new PerformanceObserver((entries) => {
        const list = entries.getEntries();
        console.log('[FID] list: ', list);
        list.forEach((entry) => console.log('[FID] value: ', entry.processingStart - entry.startTime));
    }).observe({ type: 'first-input', buffered: true });
};
    
// 获取 TTFB 指标
const getTTFB = () => {
    new PerformanceObserver((entries) => {
        const [entry] = entries.getEntriesByType('navigation');
        console.log('[TTFB] entry ==> ', entry);
        console.log('[TTFB] value: ', entry.responseStart);
    }).observe({ type: 'navigation', buffered: true });
};
    
// 获取 CLS 指标
const getCLS = () => {
    new PerformanceObserver((entries) => {
        const list = entries.getEntries();
        console.log('[getCLS] list ===> ', list);
        list.forEach(entry => console.log('[getCLS] value: ', entry.value));
    }).observe({ type: 'layout-shift', buffered: true });
};

打开掘金首页,运行上述代码:

puppeteer 获取

像开头说到的,由于要打开任意一个页面地址获取其性能指标数据。在写好获取性能指标的js脚本后,那么就有两种方式获取:

  • 一种是每个需要获取性能指标的页面都引入这个脚本文件
  • 另一种就是打开页面时动态注入脚本文件。

显然,第二种方式会好一些。

因为项目内是使用 puppeteer 来做UI自动化相关的事情,自然这次就利用 puppeteer来完成动态注入脚本,获取性能指标的事情。

puppeteer 简介

简单的说,puppeteer 是一个可以在 node 环境里运行浏览器的 npm包,它提供了一系列 API 来模拟手动的 鼠标点击,按键按下/松开,屏幕截图,请求拦截等操作。

安装

js 复制代码
npm i puppeteer

需要留意的是,puppeteer 下载过程中,会下载 Chromium 浏览器,所以耗时会比较长,而且很有可能会中途下载失败。所以一般下载 puppeteer 有以下几种思路:

运行

puppeteer 的运行很简单,有手就行。和我们平时使用浏览器一样,平时我们都是 "打开浏览器 -> 输入网址 -> 打开页面 -> 一系列操作(截屏,选中文本,下载图片等)-> 关闭浏览器"。那么使用 puppeteer 来运行浏览器也是一样的,只不过是改为使用代码来操作而已,下面以一个截图小例子来演示:

js 复制代码
import puppeteer from 'puppeteer';
import * as path from 'path';

(
  async () => {
    // 打开浏览器
    const browser = await puppeteer.launch({
      headless: false,
      defaultViewport: { width: 0, height: 0 }
    });
    // 新建一个页面
    const page = await browser.newPage();
    // 跳到相应的网址
    await page.goto('https://juejin.cn', { waitUntil: ['load', 'networkidle0'] });
    // 一系列操作: 截图
    await page.screenshot({ path: path.join(__dirname, '../1.png'), fullPage: true });
    // 关闭浏览器
    await browser.close();
  }
)()

代码里的各个方法的参数,不必纠结需要传哪些值,值是布尔值还是其它。因为在使用了typescript的情况下,直接在VSCode里按住Ctrl点击方法名就可以看到方法所需的参数了:

再不济,直接看官方文档就行。

获取性能指标

经过上面的截图例子,相信大家已经对puppeteer有了初步的了解,接下来就正式开始进入获取性能指标数据的戏份了。

利用启动项优化执行性能

我们知道,Chrome浏览器比较占用内存,所以我们可以利用launch方法的参数进行一些启动优化,把部分功能禁用掉,比如GPU、Sandbox、插件等,减少内存的使用和相关计算等。详见:Puppeteer性能优化与执行速度提升

Puppeteer自身不会消耗太多资源,耗费资源的大户是Chromium Headless。所以需要理解Chromium运行的原理,才能方便优化。

Chromium消耗最多的资源是CPU,一是渲染需要大量计算,二是Dom的解析与渲染在不同的进程,进程间切换会给CPU造成压力(进程多了之后特别明显)。其次消耗最多的是内存,Chromium是以多进程的方式运行,一个页面会生成一个进程,一个进程占用30M左右的内存,大致估算1000个请求占用30G内存,在并发高的时候内存瓶颈最先显现。

优化最终会落在内存和CPU上(所有软件的优化最终都要落到这里),通常来说因为并发造成的瓶颈需要优化内存,计算速度慢的问题要优化CPU。使用Puppeteer的用户多半会更关心计算速度,所以下面我们谈谈如何优化Puppeteer的计算速度。

js 复制代码
    const browser = await puppeteer.launch({
      headless: false,
      defaultViewport: { width: 0, height: 0 },
      // 禁用掉部分功能来提升执行性能
      args: [
        '--start-maximized',
        '--disable-gpu', 
        '--disable-dev-shm-usage',
        '--disable-setuid-sandbox',
        '--no-first-run',
        '--no-sandbox',
        '--no-zygote',
        '--disable-web-security',
      ],
    });

可以看到,使用了一堆字符串来禁用部分功能,不过还好基本都是见名知意的,例如:--disable-gpu明显就是禁用GPU。如果你想深入了解都有哪些值可以使用,可以参见:chromium-command-line-switches,里面罗列所有可以使用的值:

编写性能指标获取脚本

获取上面说过了,这里就不再展开了,直接给出代码示例代码:

获取自定义性能指标
js 复制代码
 // collectCustomVitals.js
 // 重定向耗时
const redirectTime = ({ redirectEnd, redirectStart }) => (redirectEnd - redirectStart);

// DNS寻址耗时
const domainLookupTime = ({ domainLookupEnd, domainLookupStart }) => (domainLookupEnd - domainLookupStart);

// TCP连接耗时
const tcpConnectTime = ({ connectEnd, connectStart }) => (connectEnd - connectStart);

// TLS连接耗时
const tlsConnectTime = ({ connectEnd, secureConnectionStart, name }) => (
    /^https:/.test(name) ? (connectEnd - secureConnectionStart) : 0
  )

// 资源请求总耗时
const requestToResponseTime = ({ responseEnd, requestStart }) => (responseEnd - requestStart);

// 资源下载耗时
const downloadTime = ({ responseEnd, responseStart }) => (responseEnd - responseStart);

// 资源获取总耗时
const fetchTime = ({ responseEnd, startTime }) => (responseEnd - startTime);

// 收集自定义页面数据
const collectPerformanceResourceTimingData = () => {
  // 函数映射
  const collectMap = {
    redirectTime,
    domainLookupTime,
    tcpConnectTime,
    tlsConnectTime,
    requestToResponseTime,
    downloadTime,
    fetchTime,
  };

  // 获取 key-value 集合
  const vitalsEntries = Object.entries(collectMap);

  /** 
   *  计算自定义性能指标
   * @param {Object} entry 性能指标数据对象
   * @returns 自定义的目标性能指标对象
   */
  const calc = (entry) => {
    const calcWithVitals = (data, [key, handler]) => ({ ...data, [key]: handler(entry)});
    return vitalsEntries.reduce(calcWithVitals, { name: entry.name });
  }

  /**
   * 收集性能指标对象
   * @param {Array} list 存放自定义性能指标对象的数组
   * @param {Object} entry 性能指标对象
   * @returns 存放自定义性能指标对象的数组
   */
  const collect = (list, entry) => ([...list, calc(entry)]);

  // 利用 getEntriesByType 获取页面资源的性能指标数组
  const list = performance.getEntriesByType('resource');

  return  list.reduce(collect, []);
};

// 赋值到 window 对象,供动态注入时直接调用
window.collectPerformanceResourceTimingData = collectPerformanceResourceTimingData;

可拷贝代码直接打开掘金测试下:

获取核心性能指标

上面我们提到过,获取FCPLCP .......这几个指标,可以借助 PerformanceObserver API获取,但是这里我们不使用这个API获取,而是使用 web-vitals 这个 npm包来获取,因为它做了许多兼容逻辑,而且使用也很方便:

我们把它下载下来看看,下载地址:web-vitals

发现都在一行,找个js在线格式化网站将代码格式化一下,另外保存到名为web-vitals.js的文件里再看看:

嗯,这样清晰多了。

再本地随便写个 html 文档,引入这个脚本文件测试下:

发现报错了,提示 export 有问题。解决起来也很简单,就是给引入脚本的 script 标签加上 type='module' 即可:

但这里为了之后使用方便,我直接将所有方法挂在到 window 对象上:

再借助 web-vitals 里的 getFCPgetLCPgetFIDgetTTFBgetCLS 这些方法,编写获取核心性能指标数据的函数:

js 复制代码
// 收集核心性能指标数据
const collectWebVitalsData = () => {
   const collect = (entry) => {
       if(!window.webVitalsData) {
           window.webVitalsData = {};
       }
       window.webVitalsData[entry.name] = entry.value;
   }

   window.getFCP(collect);
   window.getLCP(collect);
   window.getFID(collect);
   window.getTTFB(collect);
   window.getCLS(collect);
}

// 赋值到 window 对象,供动态注入时直接调用
window.collectWebVitalsData = collectWebVitalsData;

这段逻辑放到刚刚的web-vitals.js文件里面去:

输入任意网址

这里原意是在项目的前端页面上输入任意一个系统内的页面地址,利用 webSocket 发送页面地址到后端,在后端环境获取页面性能指标。

但是限于篇幅,这里就不搭建一个前端项目了,而是使用 readline 这个包,在终端上手动输入网址来模拟。readline 的使用也很简单,可参见:Node.js之readline模块的使用

还是使用上面那个截图的例子来演示,结合 readline 如何在终端输入网址进行截图。

首先,把截图逻辑抽取成一个函数:

js 复制代码
// 具体代码逻辑查看上面提到的地方
 const collect = async (website: string) => {
    // 打开浏览器
    ......
    // 新建一个页面
    ......
    // 跳到相应的网址
    ......
    // 一系列操作: 截图
    ......
    // 关闭浏览器
    ......
}

再引入 readline ,写个从终端获取输入的逻辑:

js 复制代码
import * as readline from 'readline';
    
const inputWebsite = () => {
  // 创建实例
  const rl = readline.createInterface({
    input: process.stdin, // 读取终端输入内容
    output: process.stdout, // 输出到终端
    prompt: '请输入网址:\n', // 提示文案
  });

  rl.prompt(); // 展示提示文案
  // 监听输入
  rl.on('line', (input) => {
    // 内容不为空则执行截图的逻辑
    input && collect(input);
  })
}

测试看看:

获取性能指标

动态注入脚本

这里需要使用到 addScriptTag 这个API。见名知意,这个方法就是用来动态给页面注入javascript脚本的:

看看它的入参有哪些,就知道应该怎么用了:

由于我们已经提前写好了脚本文件,所以这里我们主要用到path字段就可以了,为了后续看测试效果,这里还用到了id字段,给插入的script标签添加个id:

js 复制代码
// 动态注入脚本
const injectScript = async (page: Page) => {
  await page.addScriptTag({
    path: path.join(__dirname, './web-vitals.js'),
    id: 'web-vitals',
  });
  await page.addScriptTag({
    path: path.join(__dirname, './collectCustomVitals.js'),
    id: 'collectCustomVitals',
  });
}

测试验证看看:

js 复制代码
// 收集核心性能指标
const collectPerformanceData = async (webSite: string) => {
  // 打开浏览器
  const browser = await launchBrowser();
  // 新建页面
  const page: Page = await browser.newPage();
  // 打开页面
  await page.goto(webSite, { waitUntil: ['load', 'networkidle0'] });
  // 注入脚本
  await injectScript(page);
  // 关闭浏览器
  // await browser.close();
} 

可以看到,掘金页面被插入了我们编写好的两个javascript脚本,打开控制台,运行在两文件里分别定义的collectPerformanceResourceTimingDatacollectWebVitalsData方法,都是可以正常执行且拿到数据的。

收集性能指标

动态注入脚本的事情完成了,接下来就到了获取数据的时候了。这里用到的APIevaluate,它的作用时在浏览器环境中执行我们提供的回调函数并且返回回调函数的返回值:

利用这个API,我们可以写一个回调函数提供给它,在回调函数里调用collectPerformanceResourceTimingDatacollectWebVitalsData方法,分别用于获取自定义性能指标数据和注入回调函数获取核心性能指标数据:

js 复制代码
// 收集性能指标数据
const collectData = async (browser: Browser, page: Page) => {
  // 先调用一次 collectWebVitalsData ,注册回调函数
  await page.evaluate(() => (window as any).collectWebVitalsData());

  const res = await page.evaluate(() => {
    const w = window as any;
    return {
      performanceResourceTimingData: w.collectPerformanceResourceTimingData(),
      webVitalsData: w.webVitalsData,
    }
  });
  console.log('[collectData] ==> ', res);
  return res;
}

测试看看:

可以看到,控制台里打印出的内容正是我们预期的内容。

可是 webVitalsData 没有返回全,少了LCPFIDCLS字段。如果你对上面介绍这些字段的描述还有印象的话,就知道原因了:FID需要用户与页面首次交互才会触发,CLS在页面切后台再切回来的时候会触发,而实践中发现,LCP触发方式和CLS一样。所以为了拿到这三个字段信息,我们需要做两件事情:

  • 模拟用户与页面进行交互
  • 模拟页面切后台再切回前台

这里需要分别使用到两个API: click以及bringToFront。前者用于模拟鼠标点击,后者用于模拟标签页切换。

模拟鼠标点击页面,触发FID指标事件:

js 复制代码
// 为了获取FID,模拟鼠标点击
const inOrderToTriggerFID = async (page: Page) => {
  const viewPort = { width: 1920, height: 1080 };
  await page.setViewport(viewPort);
  console.log('[inOrderToTriggerFID] ==> ', viewPort);
  await page.mouse.click(
    viewPort.width / 2,
    viewPort.height / 2,
  )
  await wait(1000);
}; 

这里要做的事情就是:利用页面 pagemouse 属性的 click(x, y) 方法去点击页面某个地方,即(x, y)坐标位置。为了确保点击的位置不超出页面区域,首先得利用 setViewport API设置一下页面的大小,再点击页面的中间位置,即(viewPort.width / 2, viewPort.height / 2)坐标位置。

模拟页面切到后台再切回前台,触发CLSLCP指标事件:

js 复制代码
// 为了获取 CLS 和 LCP,模拟切页面
const inOrderToTriggerCLSAndLCP = async (browser: Browser, page: Page) => {
  const [firstPage] = await browser.pages();
  await firstPage.bringToFront();
  await wait(1000);
  await page.bringToFront();
  await wait(1000);
}

这里要做的事情就是:利用浏览器实例 browserpages 方法,拿到当前浏览器打开的所有页面中的第一个空白页实例firstPage。然后调用 bringToFront 方法,将firstPage切换到前台,再将目标页面page用同样的方法切到前台。这样通过两个页面分别切换到前台的操作,模拟实现了目标页面切后台再切回前台的操作。

把以上两段逻辑,在收集性能指标数据的方法里调用,就可以获取到所有的核心新能指标数据了:

js 复制代码
// 收集性能指标数据
const collectData = async (browser: Browser, page: Page) => {
  // 触发 FID
  await inOrderToTriggerFID(page);
  // 触发 CLS 和 LCP
  await inOrderToTriggerCLSAndLCP(browser, page);
  // 先调用一次 collectWebVitalsData ,注册回调函数
  await page.evaluate(() => (window as any).collectWebVitalsData());
  // 触发 CLS 和 LCP
  await inOrderToTriggerCLSAndLCP(browser, page);
  const res = await page.evaluate(() => {
    const w = window as any;
    return {
      performanceResourceTimingData: w.collectPerformanceResourceTimingData(),
      webVitalsData: w.webVitalsData,
    }
  });
  console.log('[collectData] ==> ', res.webVitalsData);
  return res;
}

测试看看能不能获取到所有的核心性能指标数据:

可以看到,5个核心性能指标数据都拿到了。

小结

经过上述两个步骤的实践,我们成功的把获取性能指标数据的脚本注入到了目标页面,并且顺利地拿到了我们列出的自定义性能指标数据以及核心性能指标数据。数据拿到了,后续怎么使用就拿捏了,可以发送到前端项目以可视化的方式展示,也可以提交给后端做数据分析用。

坑点

刷新页面丢失动态插入脚本

上面我们已经实现了动态注入脚本到目标页面了,但是后面发现个问题。因为要区分使用缓存以及不使用缓存的情况下,页面资源加载速度的对比。于是就利用 setCacheEnabled 设置页面是否可以使用缓存,再刷新页面获取第二遍页面性能数据,发现刷新后,动态注入的脚本就没了。

解决这个问题很简单,就是在页面重新加载后,重新注入一遍脚本:

js 复制代码
// 动态注入脚本
const injectScript = async (page: Page) => {
  // 注入脚本
  const inject = async () => {
    await page.addScriptTag({
      path: path.join(__dirname, './web-vitals.js'),
      id: 'web-vitals',
    });
    await page.addScriptTag({
      path: path.join(__dirname, './collectCustomVitals.js'),
      id: 'collectCustomVitals',
    });
  };

  await inject();
  page.on('load', async () => await inject());
  await wait(1000);
}

把注入脚本的逻辑封装到inject函数里,执行injectScript方法时,先调用inject,在通过页面实例的on方法,监听页面的加载事件load,把inject方法作为回调。这样当页面刷新后,就会触发这个监听事件,重新注入脚本了。

直接返回performance数据异常

一开始,我本打算在 evaluate API 里直接返回 getEntriesByType 获取到的数据,然后再进行处理:

js 复制代码
// 收集性能指标数据
const collectData = async (browser: Browser, page: Page) => {
  const res = await page.evaluate(() => {
    console.log('[performance.getEntriesByType]', performance.getEntriesByType('navigation'));
    return {
      performanceResourceTimingData: performance.getEntriesByType('navigation'),
    }
  });
  console.log('[collectData] ==> ', res);
  // 后续的数据处理逻辑
  ...
  return res;
}

但是发现返回来的数据都是空数据,而页面里打印确实是有的: 这个问题暂时不得而知,推测是 getEntriesByType 返回的数据,复制传递过程中会出错,所以返回了空数据。因为当时在页面获取到数据后,发现通过 webSocket 发送给另一个项目,得到的是空数据,而页面又能打印出数据。后续没办法,只能把数据处理逻辑在浏览器环境里执行,然后再返回计算结果。

总结

本文通过一个请求总过程,列出了从开始到结束分别会有这些过程:

重定向、构建请求、查看缓存、DNS解析、TCP队列等待、建立TCP连接、建立TLS连接、发送请求、服务器处理请求、服务器响应数据、断开TCP连接。

结合 performancegetEntriesByType API,在这些过程中,会有诸如 redirectStartredirectEndrequestStartresponseStartresponseEnd 等字段记录时间戳。根据这些字段,我们可以自定义所需的性能指标数据。

除此之外,页面还有一些核心的性能指标:FCPLCPFIDTTFBCLS等,介绍了这些核心指标在什么时机会触发,衡量指标是什么。虽然通过 PerformanceObserver API 可以获取,但是为了更方便以及兼容性更好,改为了使用 web-vitals 这个 npm包来获取。

最后基于所在项目的技术栈,通过例子介绍了怎么借助puppeteer 来动态注入脚本,收集数据,以及过程中的一些坑点。

希望阅读完本文,能给大家带来帮助~~

相关推荐
MarkHD3 小时前
javascript 常见设计模式
开发语言·javascript·设计模式
托尼沙滩裤3 小时前
【js面试题】js的数据结构
前端·javascript·数据结构
朝阳394 小时前
vue3【实战】来回拖拽放置图片
javascript·vue.js
不如喫茶去4 小时前
VUE自定义新增、复制、删除dom元素
前端·javascript·vue.js
阿垚啊4 小时前
vue事件参数
前端·javascript·vue.js
加仑小铁4 小时前
【区分vue2和vue3下的element UI Dialog 对话框组件,分别详细介绍属性,事件,方法如何使用,并举例】
javascript·vue.js·ui
Simaoya6 小时前
vue判断元素滚动到底部后加载更多
前端·javascript·vue.js
头顶一只喵喵6 小时前
Vue基础知识:Vue3.3出现的defineOptions,如何使用,解决了什么问题?
前端·javascript·vue.js·vue3
掘金安东尼6 小时前
上周前端发生哪些新鲜事儿?#370
前端·javascript·面试
黑色的糖果6 小时前
echarts横向立体3D柱状图
前端·javascript·echarts