# 性能优化的意义

# 性能是留住用户很重要的一环

  • pinterest 重建了他们的页面以实现性能,使感知等待时间减少了 40%,从而将搜索引擎流量和注册量提高了 15%
  • 通过将平均页面加载时间减少 850 毫秒,COOK 发现他们能够将转化率提高 7%,将跳出率降低 7%,并将每个页面的页面数量增加 10%。
  • BBC 发现,他们在网站加载的每一秒钟内就会损失 10% 的用户。
  • DoubleClick by Google 发现,如果网页加载时间超过 3 秒,则会有 53% 的用户放弃移动网站的访问。

# 性能是改善转换率至关重要的一环

  • 对于 Mobify,主页加载速度每减少 100 毫秒,基于会话的转换增加 1.11%,平均年收入增加近 380,000 美元。此外,结账页面加载速度减少 100 毫秒,基于会话的转换增加了 1.55%,从而使年均收入增长近 530,000 美元
  • DoubleClick 发现在 5 秒内加载网站的发布商的广告收入是在 19 秒内加载的网站的两倍。. 当 AutoAnything 将页面加载时间缩短一半时,他们的销售额增长了 12-13%

# 用户角度的性能标准是什么

# 著名的 2-5-8 原则

  • 当用户能够在 2 秒以内得到响应时,会感觉系统的响应很快;
  • 当用户在 2-5 秒之间得到响应时,会感觉系统的响应速度还可以;
  • 当用户在 5-8 秒以内得到响应时,会感觉系统的响应速度很慢,但是还可以接受;
  • 而当用户在超过 8 秒后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应,而选择离开这个 Web 站点,或者发起第二次请求。

综上所述:一个网站的性能好坏是留住用户和实现变现的基础

而我们的目标就是力争 1s,保住 2s

1s 的差距,看似微乎其微,但这 1s,浏览器实际上可以做非常多的事情 接下来让我们来看看如何对一个网站进行性能分析

# RAIL模型

RAILResponseAnimationIdleLoad的首字母缩写,是一种由Google Chrome团队于2015年提出的性能模型,用于提升浏览器的用户体验和性能。

RAIL模型的理念是以用户为中心,最终目标并不是让你的网站在任何特定设备上都能运行很快,而是使用户满意。

Response: 应该尽可能快速的响应用户的操作,应在在100ms以内响应用户输入。

Animation: 在展示动画的时候,每一帧应该以16ms进行渲染,这样可以保持动画效果的一致性,并且避免卡顿。

Idle: 当使用js主线程的时候,应该把任务划分到执行时间小于50ms的片段中去,这样可以释放线程以进行用户交互。50ms为单位是为了保证用户在发生操作的100ms内做出响应。

要使网站响应迅速,动画流畅,通常都需要较长的处理时间,但以用户为中心来看待性能问题,就会发现并非所有工作都需要在响应和加载阶段完成,完全可以利用浏览器的空闲时间处理可延迟的任务,只要让用户感受不到延迟即可。利用空闲时间处理延迟可减少预加载的数据大小,以保证网站或应用快速完成加载。

Load: 应该在小于1s的时间内加载完成你的网站,并可以进行用户交互。根据网络条件和硬件的不同,用户对性能延迟的理解也有所不同,在3G网络需要花费更多的时间,5s是一个更现实的目标。

# 常见性能指标

# 1. FP 白屏(First Paint )

首次绘制,是时间线上的第一个时间点,它代表网页的第一个像素渲染到屏幕上所用时间,也就是页面在屏幕上首次发生视觉变化的时间

白屏时间过长,会让用户认为我们的页面不能用或者可用性差

统计逻辑

通过performance.getEntriesByType('paint'),取第一个paint的时间。如:

function getFPTime(){
    const timings = performance.getEntriesByType('paint')[0];
    return timings ? Math.round(timings.startTime) : null
} 

# 2.FCP 首屏(first contentful paint )

首次内容绘制,它代表浏览器第一次向屏幕绘内容。注意:只有首次绘制文本、图片(包含背景图)、非白色的canvasSVG时才被算作FCP

FCP时间(秒) 颜色编码 FPC分数
0 - 2 绿色(快) 75 - 100
2 - 4 橙色(中等) 50 - 74
超过4 红色(慢) 0 - 49

统计逻辑

通过performance.getEntriesByType('paint’),取第二个paint的时间,或者通过Mutation Observer观察到首次节点变动的时间。如

function getFPTime(){
    const timings = performance.getEntriesByType('paint');
    if(timings.length > 1)return timings[1]
    return timings ? Math.round(timings.startTime) : null
} 
function getFPTime(){
  const domEntries = []
  const observer = new MutationObserver((mutationsList)=>{
      for(var mutation of mutationsList) {
          if (mutation.type == 'childList') {
              console.log('A child node has been added or removed.');
          }
          if (mutation.type == 'addedNodes') {
              //TODO新增了节点,做处理,计算此时的可见性/位置/出现时间等信息,然后 push 进数组
              ...
              domEntries.push(mutation) 
          }
      }
  });
  
   //伪代码,算 DOM 变化时的最小那个时间,即节点首次变动的时间
   return Math.round(domEntries.length ? Math.min(...domEntries.map(entry => entry.time)) : 0);
}

注意

  • FPFCP这两个指标之间的主要区别是:FP是当浏览器开始绘制内容到屏幕上的时候,只要在视觉上开始发生变化,无论是什么内容触发的视觉变化,在这一刻,这个时间点,叫做FP
  • 相比之下,FCP指的是浏览器首次绘制来自DOM的内容。例如:文本,图片,SVG,canvas元素等,这个时间点叫FCP
  • FPFCP可能是相同的时间,也可能是先FPFCP

# 3. FMP 首次有意义的绘制(First Meaningful Paint )

是页面主要内容出现在屏幕上的时间, 这是用户感知加载体验的主要指标。目前尚无标准化的定义, 因为很难以通用的方式去确定各种类型页面的关键内容(只是记录了加载体验的最开始。如果页面显示的是启动图片或者 loading 动画,这个时刻对用用户而言没有意义))

统计逻辑

  • 目前没有统一逻辑,阿里有个标准为最高可见增量元素,采用深度优先遍历方法,详细可见 (opens new window)
  • 其次,可以自定义,比如通 Mutation Observer 观察页面加载的一段时间(如前20s)内页面节点的变化, 即元素新增/移除/隐藏等变化记录下来,这样可以得到页面元素的可见时间点及元素与可视区域的交叉信息等。
  • 然后,自定义一个计算公式,比如根据元素的类型进行权重取值(div 权重1img 权重2等), 然后取元素与可视区域的交叉区域面积、可见度、 权重值之间的乘积为元素评分。
  • 根据上面得到的信息, 以时间点为X轴, 该时间点可见元素的评分总和为Y轴, 取最高点对应的最小时间为页面主要内容出现在屏幕上的时间

# 4.LCP(Largest Contentful Paint )

最大内容绘制,可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户的可见时间。img图片,video元素的封面,通过url加载到的背景,文本节点等,为了提供更好的用户体验,网站应该在2.5s以内或者更短的时间最大内容绘制

LCP时间(秒) 颜色编码
0 - 2.5 绿色(快)
2.5 - 4 橙色(中等)
超过4 红色(慢)

# 5. FID (first Input Delay)

首次输入延迟,是测量用户首次与您的站点交互时的时间(即当他们单击链接/点击按钮/使用自定义的JavaScript驱动控件时)到浏览器实际能够回应这种互动的时间

输入延迟是因为浏览器的主线程正忙于做其他事情,所以不能响应用户,发生这种情况的一个常见原因是浏览器正忙于解析和执行应用程序加载的大量计算的JavaScript

FID时间(毫秒) 颜色编码
0 - 100 绿色(快)
100 - 300 橙色(中等)
超过300 红色(慢)

统计逻辑

  • 方式一,通过performanceObserver(目前支持性为88.78%)观察类型为first-input的entry,获得其startTime/duration等数即可
  • 方式二,初始化时为特定事件类型(click/touch/keydown)绑定通用统计逻辑事件,开始调用时从event.timeStamp取开始处理的时间(这个时间就是首次输入延迟时间),在事件处理中注册requestIdleCallback事件回调onIdleCallback,当onIdleCallback被执行时,当前时间减开始的event.timeStamp即为duration时间
// 方式一
function getFIDTime(){
    const timings = performance.getEntriesByType('first-input')[0];
    return timings ? timings : null
}

// 方式二,以下代码仅代表思路
['click','touch','keydown'].forEach(eventType => {
    window.addEventListener(eventType, eventHandle);
});

function eventHandle(e) {
    const eventTime = e.timeStamp;
    window.requestIdleCallback(onIdleCallback.bind(this, eventTime, e));
}

function onIdleCallback(eventTime, e) {
    const now = window.performance.now();
    const duration = now - eventTime;

    return {
        duration: Math.round(duration),
        timestamp: Math.round(eventTime)
    }

    ['click','touch','keydown'].forEach(eventType => {
        window.removeEventListener(eventType, eventHandle);
    });
}

Event 对象的更多理解 (opens new window)

# 6.TTI (Time To Internative)可交互时间

可交互时间,指的是应用在视觉上都已渲染出了,完全可以响应用户的输入了。是衡量应用加载所需时间并能够快速响应用户交互的指标

TTI时间(秒) 颜色编码
0 - 3.8 绿色(快)
3.9 - 7.3 橙色(中等)
超过7.3 红色(慢)

统计逻辑

FMP 相同,很难规范化适用于所有网页的 TTI 指标定义。

统计方式一:谷歌实验室写的npm包,tti-polyfill

import ttiPolyfill from 'tti-polyfill';

ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
  ga('send', 'event', {
    eventCategory:'Performance Metrics',
    eventAction:'TTI',
    eventValue: tti,
    nonInteraction: true,
  });
});

统计方式二:在页面加载的一定时间内(如前50s内),以(domContentLoadedEventStart-navigationStart)+5为起始点,循环寻找,找到一个5s的窗口,其中网络请求不超过2个且没有长任务(>50ms),再找到该5秒窗口之前的最后一个长任务,该长任务结束的时间点就是可稳定交互时间。其中长任务可自定义时间或通performance.getEntriesByType('longtask')获取

// 以下代码仅代表思路
const basicTime = 5000;

function getTTITime(startTime,longTaskEntries, resourceEntries,domContentLoadedTime) {

  let busyNetworkInWindow = [];
  let tti = startTime;

  while (startTime + basicTime <= 50000) { //从前50s 中去找
    tti = startTime;
    longTasksInWindow = longTaskEntries.filter(task => {
      return task.startTime < startTime + basicTime && task.startTime + task.duration > startTime;
    });
    if (longTasksInWindow.length) {
      const lastLongTask = longTasksInWindow[longTasksInWindow.length - 1];
      startTime = lastLongTask.startTime + lastLongTask.duration;
      continue;
    }
    busyNetworkInWindow = resourceEntries.filter(request => {
      return !(request.startTime >= startTime + basicTime || request.startTime + request.duration <= startTime);
    });
    if (busyNetworkInWindow.length > 2) {
      const firstRequest = busyNetworkInWindow[0];
      startTime = firstRequest.startTime + firstRequest.duration;
      continue;
    }
    return Math.max(tti, domContentLoadedTime);
  }
  return Math.max(tti, domContentLoadedTime);
}

# 7. TBT(total blocking time )

总阻塞时间,度量了FCPTTI之间的总时间,在该时间范围内,主线程被阻塞足够长的时间以防止输入响应。只要存在长任务,该主线程就会被视为阻塞,该任务在主线程上运行超过50毫秒。

线程阻塞是因为浏览器无法中断正在进行的任务,因此如果用户确实在较长的任务中间与页面进行交互,则浏览器必须等待任务完成才能响应

TBT时间(毫秒) 颜色编码
0 - 300 绿色(快)
300 - 600 橙色(中等)
超过600 红色(慢)

# 8. CLS(Cumulative Layout Shift)

累计布局位移,CLS会测量在页面整个生命周期中发生的每个意外的布局移位的所有单独布局移位分数的总和,他是一种保证页面的视觉稳定性从而提升用户体验的指标方案。

用人话来说就是当点击页面中的某个元素的时候,突然布局变了,手指点到了其它位置。比如想点击页面的链接,突然出现了一个banner。这种情况可能是因为尺寸未知的图像或者视频。

CLS时间(毫秒) 颜色编码
0 - 0.1 绿色(快)
0.1 - 0.25 橙色(中等)
超过0.25 红色(慢)

9. FCI(First CPU Idle)首次CPU空闲时间

代表着一个网页已经满足了最小程度的与用户发生交互行为的时刻。当我们打开一个网页,我们并不需要等到一个网页完全加载好了,每一个元素都已经完成了渲染,然后再去与网页进行交互行为。网页满足了我们基本的交互的时间点是衡量网页性能的一个重要指标

统计逻辑

FCI为在FMP之后,首次在一定窗口时间内没有长任务发生的那一时刻,并且如果这个时间点早于DOMContentLoaded时间,那么FCI的时间为DOMContentLoaded时间,窗口时间的计算函数可以根据Lighthouse提供的计算公式 N = f(t) = 4 * e^(-0.045 * t) + 1 进行自定义设计

# 10. FPS(Frames Per Second)每秒帧率

表示的是每秒钟画面更新次数,当今大多数设备的屏幕刷新率都是60次/秒

参考标准:

  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;
  • 帧率波动很大的动画,亦会使人感觉到卡顿

统计逻辑

利用requestAnimationFrame,循环调用,当now大于lastTime+1S时,计算FPS。若小于某个阀值则可以认为当前帧率较差,若连续小于多个阀值,则停止统计,当前页面处于卡顿状态,进入卡顿处理逻辑

更多具体参考 (opens new window)

使用 requestAnimationFrame 计算 FPS

正常而言 requestAnimationFrame 这个方法在一秒内会执行 60 次,也就是不掉帧的情况下。假设动画在时间 A 开始执行,在时间 B 结束,耗时 x ms。而中间 requestAnimationFrame 一共执行了 n 次,则此段动画的帧率大致为:n / (B - A)

核心代码如下,能近似计算每秒页面帧率,以及我们额外记录一个 allFrameCount,用于记录 rAF 的执行次数,用于计算每次动画的帧率

var rAF = function () {
    return (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 1000 / 60);
        }
    );
}();
  
var frame = 0;
var allFrameCount = 0;
var lastTime = Date.now();
var lastFameTime = Date.now();
  
var loop = function () {
    var now = Date.now();
    var fs = (now - lastFameTime);
    var fps = Math.round(1000 / fs);
  
    lastFameTime = now;
    // 不置 0,在动画的开头及结尾记录此值的差值算出 FPS
    allFrameCount++;
    frame++;
  
    if (now > 1000 + lastTime) {
        var fps = Math.round((frame * 1000) / (now - lastTime));
        console.log(`${new Date()} 1S内 FPS:`, fps);
        frame = 0;
        lastTime = now;
    };
  
    rAF(loop);
}
 
loop();

找一个有动画不断运行的页面进行测试,可以看到代码运行如下:

使用 Chrome 同时调出页面的 FPS meter(Ctrl+shift+p输入fps调出fps监控面板),对比两边的实时 FPS 值,基本吻合

演示Demo https://codepen.io/poetries/pen/gLoQzo

# 11. 长任务(Long Task)

当一个任务执行时间超过 50ms 时消耗到的任务 (50ms 阈值是从 RAIL 模型总结出来的结论,这个是 google 研究用户感知得出的结论,类似用户的感知/耐心的阈值,超过这个阈值的任务,用户会感知到页面的卡顿)

# 12. DCL (DOMContentLoaded)

当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式,图像和子框架的完成加载。

# 13. L(onLoaded)

当依赖的资源,全部加载完毕之后才会触发

上面介绍了 13 种性能指标 大家没必要搞懂每一个指标的定义 我们来看看我们需要关注的核心的几个性能指标

# Google Web Vitals - 使用者体验量化

谷歌认为之前的标准太复杂,指标太多了。Google 于 2020 年 5 年 5 日提出了新的使用者体验量化方式,推出 Web Vitals 是简化这个学习的曲线,大家只要观注 Web Vitals 指标表现即可。加载性能LCP,交互性FID,视觉稳定性CLS。只需要做好这三个,网站的性能基本上就可以了。

web-vitals 集成了 5 个指标的 api,核心指标有 3 个;

  • LCP 显示最大内容元素所需时间 (衡量网站初次载入速度)
  • FID 首次输入延迟时间 (衡量网站互动顺畅程度)
  • CLS 累计版面配置移转 (衡量网页元件视觉稳定性)

测量Web Vitals的工具有很多,比如Lighthouseweb-vitals,浏览器插件web vitals

<script type="module">
    import {getCLS, getFID,getFCP,getTTFB, getLCP} from 'https://unpkg.com/web-vitals?module';
    getCLS(console.log);
    getFID(console.log);
    getLCP(console.log);
    getFCP(console.log);
    getTTFB(console.log);
</script>

到此为止 我们知道了LCP FID CLS 这三大指标是比较核心的 但是如果我们想知道更多的性能指标测定方式 我们该怎么做呢 我们接着看

# 上报策略

# pv/uv

监听各种页面切换的情况;SPA页面,可以监听hashChange

# 性能数据/设备信息/网络状况

  • 在页面离开前上报,beforeUnload/visibilitychange/pagehide…+ sendBeancon/Ajax
  • img 标签+切片+压缩

# Performance API

Performance 是一个浏览器全局对象,提供了一组 API 用于编程式地获取程序在某些节点的性能数据。它包含一组高精度时间定义,以及配套的相关方法。我们可以直接在浏览器控制台打印window.performance 结果如下

// 获取 performance 数据
var performance = {
    // memory 是非标准属性,只在 Chrome 有
    // 我有多少内存
    memory: {
        usedJSHeapSize:  16100000, // JS 对象(包括V8引擎内部对象)占用的内存,一定小于 totalJSHeapSize
        totalJSHeapSize: 35100000, // 可使用的内存
        jsHeapSizeLimit: 793000000 // 内存大小限制
    },

    // 我从哪里来?
    navigation: {
        redirectCount: 0, // 如果有重定向的话,页面通过几次重定向跳转而来
        type: 0           // 0   即 TYPE_NAVIGATENEXT 正常进入的页面(非刷新、非重定向等)
                          // 1   即 TYPE_RELOAD       通过 window.location.reload() 刷新的页面
                          // 2   即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面(历史记录)
                          // 255 即 TYPE_UNDEFINED    非以上方式进入的页面
    },
//  核心时间相关
    timing: {
        // 在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等
        navigationStart: 1441112691935,

        // 前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0
        unloadEventStart: 0,

        // 和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳
        unloadEventEnd: 0,

        // 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0
        redirectStart: 0,

        // 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0
        redirectEnd: 0,

        // 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前
        fetchStart: 1441112692155,

        // DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupStart: 1441112692155,

        // DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupEnd: 1441112692155,

        // HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间
        connectStart: 1441112692155,

        // HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间
        // 注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过
        connectEnd: 1441112692155,

        // HTTPS 连接开始的时间,如果不是安全连接,则值为 0
        secureConnectionStart: 0,

        // HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存
        // 连接错误重连时,这里显示的也是新建立连接的时间
        requestStart: 1441112692158,

        // HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存
        responseStart: 1441112692686,

        // HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存
        responseEnd: 1441112692687,

        // 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
        domLoading: 1441112692690,

        // 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件
        // 注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源
        domInteractive: 1441112693093,

        // DOM 解析完成后,网页内资源加载开始的时间
        // 在 DOMContentLoaded 事件抛出前发生
        domContentLoadedEventStart: 1441112693093,

        // DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)
        domContentLoadedEventEnd: 1441112693101,

        // DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
        domComplete: 1441112693214,

        // load 事件发送给文档,也即 load 回调函数开始执行的时间
        // 注意如果没有绑定 load 事件,值为 0
        loadEventStart: 1441112693214,

        // load 事件的回调函数执行完毕的时间
        loadEventEnd: 1441112693215

        // 按照字母排序
        // connectEnd: 1441112692155,
        // connectStart: 1441112692155,
        // domComplete: 1441112693214,
        // domContentLoadedEventEnd: 1441112693101,
        // domContentLoadedEventStart: 1441112693093,
        // domInteractive: 1441112693093,
        // domLoading: 1441112692690,
        // domainLookupEnd: 1441112692155,
        // domainLookupStart: 1441112692155,
        // fetchStart: 1441112692155,
        // loadEventEnd: 1441112693215,
        // loadEventStart: 1441112693214,
        // navigationStart: 1441112691935,
        // redirectEnd: 0,
        // redirectStart: 0,
        // requestStart: 1441112692158,
        // responseEnd: 1441112692687,
        // responseStart: 1441112692686,
        // secureConnectionStart: 0,
        // unloadEventEnd: 0,
        // unloadEventStart: 0
    }
}

先来一张图解释下页面加载的几个关键时刻

# 使用 performance.timing 信息简单计算出网页性能数据

  • FPresponseStart - navigationStart
  • 重定向耗时:redirectEnd - redirectStart
  • DNS 查询耗时:domainLookupEnd - domainLookupStart
  • TCP 链接耗时:connectEnd - connectStart
  • HTTP 请求耗时:responseEnd - responseStart
  • 解析 dom 树耗时:domComplete - domInteractive
  • DOM ready 时间:domContentLoadedEventEnd - navigationStart
  • onloadloadEventEnd - navigationStart

# 使用performance.getEntries()获取所有资源请求的时间数据

获取所有资源请求的时间数据,这个函数返回一个按 startTime 排序的对象数组

# 使用performance.getEntriesByName(name)获取特定名称的时间数据

我们可以通过 getEntriesByName(name)提供的 api 去获取 FCP 数据

FCP = performance.getEntriesByName("first-contentful-paint")[0].startTime - navigationStart

# 使用performance.now()精确计算程序执行时间

performance.now方法返回当前网页自从performance.timing.navigationStart到当前时间之间的微秒数(毫秒的千分之一)。也就是说,它的精度可以达到 100 万分之一秒。

那么我们可以通过两次调用 最后计算出 js 某种操作的精确耗时

const start = performance.now();
doTasks(); // 这里是耗时操作
const end = performance.now();
console.log("耗时:" + (end - start) + "微秒。");

# 使用performance.mark以及performance.measure手动测量性能

https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark

// Create a bunch of marks.
performance.mark("squirrel");
performance.mark("squirrel");
performance.mark("monkey");
performance.mark("monkey");
performance.mark("dog");
performance.mark("dog");

// Get all of the PerformanceMark entries.
const allEntries = performance.getEntriesByType("mark");
console.log(allEntries.length);
// 6

// Get all of the "monkey" PerformanceMark entries.
const monkeyEntries = performance.getEntriesByName("monkey");
console.log(monkeyEntries.length);
// 2

// Clear out all of the marks.
performance.clearMarks();

# 性能测试

# lighthouse

先来介绍 lighthouse 工具,目前官方提供了 google devtools、google 插件、npm cli 方式应用。

我们发现 lighthouse 和 performance 区别还挺大滴,为啥呢?

原来 lighthouse 默认进行了节流处理。我们可以不勾选节流同时也直接点击 view trace 生成对应 performance 面板的数据。

lighthouse 主要针对 5 个方面做了分析。

# Performance 性能

列出了 FCPSPLCPTTITBICLS 六个指标

同时也提供可优化方案

# Accessibility 可访问性

可访问性:指无障碍设计,也称为网站可达性。是指所创建的网站对所有用户都可用/可访问,不管用户的生理/身体能力如何、不管用户是以何种方式访问网站。

# Best Practice 最佳实践

实际应用中,网站的安全问题

# SEO 搜索引擎优化

搜索引擎优化,是一种利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名

# Progressive Web App 轻应用-离线应用

PWA: 运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。这些应用无处不在、功能丰富,使其具有与原生应用相同的用户体验优势;

没有浏览器环境还可以使用 lighthouse 来测试性能吗 答案是 当然可以

我们再来用 npm cli 去实现 lighthouse 吧

# node cli lighthouse

项目安装 lighthouse

npm i -g lighthouse
lighthouse https://www.taobao.com

看看 cli 中 lighthouse 支持哪些命令

lighthouse --help
// 命令太多,介绍常用的几个
--output             // 文档报告输出支持html、json、csv,默认html;
--view               // 数据分析结束后以html展示
--only-categories    // 分析类别包括“accessibility, best-practices, performance, pwa, seo”
--throttling-method  // 限流方式:provide当前设备环境,devtools开发模式,simulate模拟手机
--form-factor        // 支持设备,mobile,desktop

# WebPageTest

在线web性能测试工具(https://www.webpagetest.org), 提供多地点测试。他只能测试已经发布了的网站。输入需要测试的网页地址,点击start test按钮就开始测试了,可以选择测试地理位置,测试的浏览器等。

# Chrome DevTools

# 浏览器的任务管理器

可以查看当前Chrome浏览器中,所有进程关于GPU,网络和内存空间的使用情况,这些进程包括当前打开的各个标签页,安装的各种扩展插件,以及GPU,网络,渲染等浏览器的默认进程,通过监控这些数据,可以定位可能存在内存泄露或网络资源加载异常的问题进程。

更多工具 -> 任务管理器

可以看到所有进行的进程,可以看到内存占用网络消耗。

# Google performance 面板

从上到下分别为 4 个区域

1:工具条,包含录制,刷新页面分析,清除结果等一系列操作

2:总览图,高度概括随时间线的变动,包括 FPSCPUNET

3:火焰图,从不同的角度分析框选区域 。例如:NetworkFrames,Interactions,Main

4:总体报告:精确到毫秒级的分析,以及按调用层级,事件分类的整理

# 工具条区域

上面红框从左到右 咱们把鼠标放上去可以看到几个英文单词

  • record 记录浏览器运行中的某一个时间段的表现
  • reload page 用于记录网页从最开始的加载到所有资源加载完成这个过程的性能表现。点击之后,页面会自动重新加载
  • Screenshots 屏幕快照,显示动态加载的页面图片
  • memory 查看多种内存占用变化

现在我们可以打开任意一个网站 点击第二个按钮reload page 开始分析

1. FPS: 全称 Frames Per Second,表示每秒传输帧数,是速度单位,用来分析动画的一个主要性能指标。1fps = 0.304 meter/sec(米/秒)。如上图所示,绿色竖线越高,FPS 越高。 红色表示长时间帧,可能出现卡顿掉帧。

  • 不同帧的体验
  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感; 帧率波动很大的动画,亦会使人感觉到卡顿。

2. CPU:CPU 资源。此面积图指示消耗 CPU 资源的事件类型。 图中颜色分别为(与总体报告中的 Summary 颜色数据表示一致):

  • 蓝色(Loading):表示网络通信和 HTML 解析时间。
  • 黄色(Scripting):表示 JavaScript 执行时间。
  • 紫色(Rendering):表示样式计算和布局(重排)时间。
  • 绿色(Painting):表示重绘时间。
  • 灰色(other):表示其它事件花费的时间。
  • 白色(Idle):表示空闲时间。

3. NET:每条彩色横杠表示一种资源。横杠越长,检索资源所需的时间越长。 每个横杠的浅色部分表示等待时间(从请求资源到第一个字节下载完成的时间)

# 火焰图

  1. Network:表示每个服务器资源的加载情况。
  2. Frames:表示每幅帧的运行情况,这里可以和上面总览的 FPS 结合来看
  3. Timings
  • DCL(DOMContentLoaded)表示 HTML 文档加载完成事件。当初始 HTML 文档完全加载并解析之后触发,无需等待样式、图片、子 frame 结束。作为明显的对比,load 事件是当个页面完全被加载时才触发。
  • FP(First Paint)首屏绘制,页面刚开始渲染的时间。
  • FCP(First ContentfulPaint)首屏内容绘制,首次绘制任何文本,图像,非空白 canvasSVG 的时间点。
  • FMP(First MeaningfulPaint)首屏有意义的内容绘制,这个“有意义”没有权威的规定,本质上是通过一种算法来猜测某个时间点可能是 FMP。有的理解为是最大元素绘制的时间,即同 LCP(Largest ContentfulPaint)。其中 FP、FCP、FMP 是同一条虚线,三者时间不一致。比如首次渲染过后,有可能出现 JS 阻塞,这种情况下 FCP 就会大于 FP。
  • L(Onload)页面所有资源加载完成事件。
  • LCP(Largest Contentful Paint )最大内容绘制,页面上尺寸最大的元素绘制时间

这里的指标就是对应我们前面提到的性能指标 所以可以直接在 performance 面板来看到网页的几个核心指标的数值

  1. Main:记录了渲染进程中主线程的执行记录,点击 main 可以看到某个任务执行的具体情况 是我们分析具体函数耗时最常看的面板

首先,面板中会有很多的 Task,如果是耗时长的 Task,其右上角会标红,这个时候,我们可以选中标红的 Task,然后放大,看其具体的耗时点。

放大后,这里可以看到都在做哪些操作,哪些函数耗时了多少,这里代码有压缩,看到的是压缩后的函数名。然后我们点击一下某个函数,在面板最下面,就会出现代码的信息,是哪个函数,耗时多少,在哪个文件上的第几行等。

这样我们就很方便地定位到耗时函数 然后去针对性优化

  1. Compositor 合成线程的执行记录,用来记录 html 绘制阶段 (Paint)结束后的图层合成操作

  2. Raster 光栅化线程池,用来让 GPU 执行光栅化的任务

  3. GPU 可以直观看到何时启动 GPU 加速

  4. Memory 选项,在勾选后,就会显示折线图

通过该图我们可以看到页面中的内存使用的情况,比如 JS Heap(堆),如果曲线一直在增长,则说明存在内存泄露,如果相当长的一段时间,内存曲线都是没有下降的,这里是有发生内存泄露的可能的。

其实在火焰图这块 我们主要关心上诉的 1234 核心的点就够了 另外如果想分析内存泄漏 可以勾选 memory 选项

# 总体报告

# Summary:表示各指标时间占用统计报表

这里的颜色代表的意思和总览区域里面的 cpu颜色一样的意思

这里一般来说,需要着重关注的有两个:一是黄色的区域,代表脚本执行时间,另一个是紫色的渲染时间

1.Loading 事件

内容 说明
Parse HTML 浏览器解析 HTML
Finish Loading 网络请求完成
Receive Data 请求的响应数据到达事件,如果响应数据很大(拆包),可能会多次触发该事件
Receive Response 响应头报文到达时触发
Send Request 发送网络请求时触发
  1. Scripting 事件
内容 说明
AnimationFrameFired 一个定义好的动画帧发生并开始回调处理时触发
Cancel Animation Frame 取消一个动画帧时触发
GC Event 垃圾回收时触发
DOMContentLoaded 当页面中的 DOM 内容加载并解析完毕时触发
Evaluate Script A script was evaluated.
Event JS 事件
Function Call 浏览器进入 JS 引擎时触发
Install Timer 创建计时器(调用 setTimeout()和 setInterval())时触发
Request Animation Frame A requestAnimationFrame() call scheduled a new frame
Remove Timer 清除计时器触发
Time 调用 console.time() 触发
Time End 调用 console.timeEnd() 触发
Timer Fired 定时器激活回调后触发
XHR Ready State Change 当一个异步请求为就绪状态后触发
XHR Load 当一个异步请求完成加载后触发

3.Rendering 事件

内容 说明
Invalidate layout 当 DOM 更改导致页面布局失效时触发
Layout 页面布局计算执行时触发
Recalculate style Chrome 重新计算元素样式时触发
Scroll 内嵌的视窗滚动时触发

4.Painting 事件

内容 说明
Composite Layers Chrome 的渲染引擎完成图片层合并时触发
Image Decode 一个图片资源完成解码后触发
Image Resize 一个图片被修改尺寸后触发
Paint 合并后的层被绘制到对应显示区域后触发

5.Stystem: 系统用时

6.Idle: 空闲时间

# Bottom-Up:表示事件时长排序列表(倒序)

这里有两列时间数据,一是"Self Time"代表任务自身执行所消耗的时间,二是"Total Time"代表此任务及其调用的附属子任务一共消耗的时间。这两列数据各有不同的用处,可以按自己的需求决定按哪列数据作为排序字段。

在 Activity 的右侧,部分还带有 Source Map 链接,点击之后可以定位到相应操作对应的代码。使用它可以比较方便地定位到具体的代码

# Call tree:表示事件调用顺序列表

Call Tree 中的内容,在 Bottom-Up 中也能看到,无明显的区别。

# Event Log:表示事件发生的顺序列表

Event Log 中的内容,是按顺序记录的事件日志,数据比较多。常见的优化级别中一般用不到它。如果是比较大型的应用,打开它可能会直接导致 Chrome 卡死。

除了 performance 我们还有一个更加便捷的工具 它自动帮我们分析好了性能 还给出了优化建议

# Network网络分析

Network面板是一个常被用到的工具,通过它可以获取到网站所有资源的请求情况,包括加载时间,尺寸大小,优先级设置以及HTTP缓存等信息。可以帮助开发者发现可能由于未进行有效压缩而导致资源尺寸过大的问题,未配置缓存策略导致二次请求加载时间过长的问题。

# 1. 缓存测试 Disable cache

# 2. 吞吐测试,模拟网速

# 3. Coverage

监控并统计出网站应用运行过程中代码执行的覆盖率情况。

统计的对象是JavaScript脚本文件与css样式文件,统计结果主要包括文件的字节大小,执行过程中已覆盖的代码字节数,可视化的覆盖率条形图。

根据执行结果可以发现到底哪些尺寸较大的代码文件覆盖率较低,这就意味着这些代码文件中可能存在较多的无用代码。

Ctrl + shift + p搜索 coverage 就会显示出来。

可以看到第一个文件有58%没有被使用到,第二个有95.2%没有使用到

# 4. Memory 面板

主要用于分析内存占用情况,如果出现内存泄露,那么就可能带来网站崩溃的后果。

为了更细致和准确的监控应用网站当前的内存使用情况,Chrome浏览器提供Memory面板,可以快速生成当前的堆内存快照。

结束后可以查看到内存占用大小,就可以对对应的模块进行优化。

5. FPS

另一个非常方便的工具是FPS计数,可以在页面运行时提供对FPS的实时估计。

Ctrl + Shift + P 输入 fps 选择显示渲染。就会在浏览器中出现监控面板。

还可以使用性能监视器,这是一个事实的监视器。

Ctrl + Shift + P 输入 monitor

# 性能优化路径

前端优化之前先从这样一个问题开始说起。当浏览器地址栏输入url按下回车,整个过程都发生了什么。性能优化基本也是围绕这个过程展开的。

首先浏览器接收到URL,到网络请求线程的开启,一个完整的HTTP请求发出,服务器接收到请求并转到具体的处理服务,前后台之间的HTTP交互和涉及的缓存机制,浏览器接收到数据包的关键渲染路径,js引擎的解析过程。大致就是这样一个过程。

下面来详细说说。

浏览器接收到输入的URL到开启网络请求线程,这个阶段是在浏览器内部完成的。那么什么是线程什么是进程呢?

简单来说,进程就是一个程序运行的实例,操作系统会为进程创建独立的内存,用来存放运行所需的代码和数据,而线程是进程的组成部分,每个进程至少有一个主线程及可能的若干子线程,这些线程由所需的进程进行启动和管理。

由于多个线程可以共享操作系统为其所属的同一个进程所分配的资源,所以多线程的并行处理能有效提高程序的运行效率。

只要某个线程执行出错,将会导致整个程序崩溃。进程与进程之间相互隔离,这保证了当一个进程挂起或崩溃的情况发生时并不会影响其他进程的正常运行,虽然每个进程只能访问系统分配给自己的资源,但可以通弄过IPC机制进行进程间通信。

进程所占用的资源会在其关闭后由操作系统回收,即使进程中存在某个线程产生的内存泄露,当进程退出时相关的内存资源也会被回收。线程之间可以共享所属进程的数据。

早期浏览器都是单进程的,其中的页面渲染,呈现,网络请求都通过线程来实现。前面说了只要一个线程崩溃就会导致整个进程崩溃。如果你上网较早应该有过这样的体会,一个网站卡死了整个浏览器都卡死了。单进程的浏览器存在很多的隐患,比如页面流畅度,安全性,稳定性都比较低。

后来Chrome推出了多进程浏览器,一个浏览器只有一个主进程,负责菜单栏,标题栏等页面显示,文件访问,前进后退以及子进程管理。除主进程外还有GPU进程,插件进程,网络进程,渲染进程。

渲染进程也称为浏览器内核,默认会为每个标签页开辟一个独立的渲染进程,负责将HTMLCSSJavaScript等资源转为可交互的页面,其中包含多个子线程,js引擎线程,GUI渲染线程,事件触发线程,定时触发器线程,异步http请求线程等。当打开一个标签页输入URL后所发起的网络请求就是从这个进程开始的,处于安全的考虑渲染进程存在于沙箱中。打开Chrome的任务管理器可以从中发现这些进程。

建立HTTP请求这个阶段主要分为两部分,DNS解析和通信链路的建立。简单来说首先发起请求的客户端浏览器要明确知道所要访问的服务器地址,然后建立通往该服务器地址的路径

# 1. DNS解析

DNS解析说白了就是根据host域名找到具体的IP地址,中间会经历很多的环节。

首先会查找浏览器的缓存,如果找不到就去查找系统自身的DNS缓存,在没有就去查找系统的hosts文件,再找不到就去本地域名服务器提供商查询根域名服务器,如果还是找不到就去查找com顶级域名服务器,最后会去权限域名服务器查找,都没有找到就返回报错信息。这就是DNS查找的过程,其中任何一个环节慢了都会影响后续的操作。

# 2. 网络模型

在通过DNS解析到目标服务器IP地址后,就可以简历网络连接进行资源的访问。在这个过程中涉及到网络架构模型,国际标准化组织提出了一些网络架构的模型,OSITCP/IP

OSI是七层架构,包括应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。TCP/IP简化到了四层,应用层,传输层,网络层,数据链路层。同样每一层慢了对性能都有影响。

# 3. TCP

经过网络模型之后就要建立TCP链接,主要是为了通过http对数据进行请求和发送。

由于TCP是面向有链接的通信协议,所以在传输数据之前需要建立好客户端与服务间的链接,即通常所说的三次握手。

# 4. 前后端数据交互

TCP链接建立好以后,便可通过HTTP等协议进行前后端的通信,但在实际的应用中,并非浏览器与确定IP地址的服务器之间直接通信,往往会在中间加入反向代理服务器。

# 5. 反向代理服务器

反向代理服务器根据客户的请求,从后端服务器上获取资源后提供给客户端,反向代理通过会做下面一些事,比如负载均衡,安全防火墙,加密及SSL加速,数据压缩,解决跨域,静态资源缓存。

# 6. 后端处理流程

请求经过反向代理服务器收到请求后,首先会有一层统一的验证环节,如跨域验证,安全拦截等,如果发现是不规则的请求则直接返回相应的拒绝报文。

通过验证后才会进入具体的后天程序代码执行阶段,如具体的计算数据库的操作等。

完成计算之后,后台会以一个HTTP响应数据包的形式发送回请求的前端,解说此次请求。

# 7. HTTP相关协议特性

HTTP是建立在传输层TCP协议上的应用层协议,在TCP层面上存在长链接和短连接的区别。

所谓长链接就是在客户端与服务器端简历TCP连接上可以连续发送数据包,但需要双方发送心跳检查包来维持这个链接。

短连接就是当客户端需要向服务器发送请求时,会在网络层IP协议之上建立一次链接,当请求发送并收到响应后,则断开此链接。

HTTP1.0时默认使用短连接。

HTTP1.1时默认使用长链接,但是长链接存在并发数,如果请求过多仍旧需要等待。常用的做法是将域名进行拆分,对小图标进行合并。

HTTP2.0之后便可以在一个TCP链接上请求多个资源,分割成更小的帧请求的性能再次提成。

# 8. 浏览器缓存

基于HTTP的缓存分为强缓存和协商缓存。

强缓存就是当浏览器判断出本地缓存未过期时,直接取本地缓存,无需发起请求,此时的状态为200 from cache,在HTTP1.1版本后通过头部的cache-control max-age属性值规定的过期时长来判断缓存是否失效,这比之前使用expires过期时间更准确并且安全。

协商缓存则需要浏览器发起HTTP请求,来判断浏览器本地缓存的文件是否改变。

# 9. 关键渲染路径

当经历了网络请求过程,从服务器获取到了所访问的页面文件之后,浏览器便要开始渲染服务器响应回来的内容。

首先浏览器会通过解析HTMLCSS文件来构建DOMCSSOM

浏览器接收读取到HTML文件,其实是根绝文件指定编码的原始字节,首先需要将字节转换为字符串,再将字符串转换为W3C标准规定的令牌结构,令牌就是HTML中不同标签代表不同含义的一组规则结构。然后经过词法分析将令牌转化为定义了属性和规则值的对象,最后将这些标签根据HTML表示的父子关系,连接成树形结构。

DOM树表示文档标记的属性和关系,但未包含其中各元素经过渲染后的外观呈现,这边是接下来CSSOM的职责了,与将HTML文件解析为文档对象模型的过程类似,CSS文件也会首先经历从字节到字符串,然后令牌化及词法分析后构建为层叠样式表对象模型。

这两个对象模型的构建过程是会花费时间的,可以通过浏览器的开发者工具性能选项卡查看到对应过程的耗时情况。

得到文档对象模型和层叠样式表对象之后就要进行绘制,呈现之前浏览器需要将文档对象模型和样式模型合并到一起最终形成一颗渲染树。这棵树中只包含可见的节点,比如displaynode的节点就是不包含的。

从所生成的DOM树的根节点开始向下遍历每个子节点,忽略所有不可见的节点,因为不可见的节点不会出现在渲染树中。

CSSOM中为每个可见的子节点找到对应的规则并应用。

布局节点根据所得到的渲染树,计算他们在试图设备中的具体位置和大小,这一步输出的是一个盒模型。绘制节点将每个节点的具体绘制方案转化为屏幕上的实际像素。

构建渲染树,布局,及绘制过程所需要的时间取决于实际文档的大小。文档过大,浏览器需要处理的任务就越多样式也复杂,绘制需要的时间就越长。所以关键渲染路径执行快慢,将直接影响首屏加载时间的性能指标。

当首屏渲染完成胡,用户在和网站的交互过程中,有可能通过JavaScript代码提供的用户操作接口更改渲染树的结构。一旦DOM结构发生改变,这个渲染过程就会重新执行一遍。

关键渲染路径的优化不仅是首屏性能,还有交互性能。

# 简版性能监控工具

var base = {
  log() {},
  logPackage() {},
  getLoadTime() {},
  getTimeoutRes() {},
  bindEvent() {},
  init() {}
}

var pm = (function() {
  // 向前兼容
  if (!window.performance) return base
  const pMonitor = { ...base }
  let config = {}
  const SEC = 1000
  const TIMEOUT = 10 * SEC
  const setTime = (limit = TIMEOUT) => time => time >= limit
  const getLoadTime = ({ startTime, responseEnd }) => responseEnd - startTime
  const getName = ({ name }) => name
  // 生成表单数据
  const convert2FormData = (data = {}) =>
    Object.entries(data).reduce((last, [key, value]) => {
      if (Array.isArray(value)) {
        return value.reduce((lastResult, item) => {
          lastResult.append(`${key}[]`, item)
          return lastResult
        }, last)
      }
      last.append(key, value)
      return last
    }, new FormData())
  // 拼接 GET 时的url
  const makeItStr = (data = {}) =>
    Object.entries(data)
      .map(([k, v]) => `${k}=${v}`)
      .join('&')
  pMonitor.getLoadTime = () => {
    const [{ domComplete }] = performance.getEntriesByType('navigation')
    return domComplete
  }
  pMonitor.getTimeoutRes = (limit = TIMEOUT) => {
    const isTimeout = setTime(limit)
    const resourceTimes = performance.getEntriesByType('resource')
    return resourceTimes
      .filter(item => isTimeout(getLoadTime(item)))
      .map(getName)
  }
  // 上报数据
  pMonitor.log = (url, data = {}, type = 'POST') => {
    const method = type.toLowerCase()
    const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url
    const body = method === 'get' ? {} : { body: convert2FormData(data) }
    const init = {
      method,
      ...body
    }
    fetch(urlToUse, init).catch(e => console.log(e))
  }
  // 封装一个上报两项核心数据的方法
  pMonitor.logPackage = () => {
    const { url, timeoutUrl, method } = config
    const domComplete = pMonitor.getLoadTime()
    const timeoutRes = pMonitor.getTimeoutRes(config.timeout)
    // 上报页面加载时间
    pMonitor.log(url, { domComplete }, method)
    if (timeoutRes.length) {
      pMonitor.log(
        timeoutUrl,
        {
          timeoutRes
        },
        method
      )
    }
  }
  // 事件绑定
  pMonitor.bindEvent = () => {
    const oldOnload = window.onload
    window.onload = e => {
      if (oldOnload && typeof oldOnload === 'function') {
        oldOnload(e)
      }
      // 尽量不影响页面主线程
      if (window.requestIdleCallback) {
        window.requestIdleCallback(pMonitor.logPackage)
      } else {
        setTimeout(pMonitor.logPackage)
      }
    }
  }

  /**
   * @param {object} option
   * @param {string} option.url 页面加载数据的上报地址
   * @param {string} option.timeoutUrl 页面资源超时的上报地址
   * @param {string=} [option.method='POST'] 请求方式
   * @param {number=} [option.timeout=10000]
   */
  pMonitor.init = option => {
    const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
    config = {
      url,
      timeoutUrl,
      method,
      timeout
    }
    // 绑定事件 用于触发上报数据
    pMonitor.bindEvent()
  }

  return pMonitor
})()
阅读全文
Last Updated: 4/11/2024, 12:16:12 PM