前端常见性能指标

文档流加载生命周期

DOMContentLoaded

是指页面元素加载完毕,但是一些资源比如图片还无法看到,但是这个时候页面是可以正常交互的,比如滚动,输入字符等。 jQuery 中经常使用的 $(document).ready() 其实监听的就是 DOMContentLoaded 事件。

load

是指页面上所有的资源(图片,音频,视频等)加载完成。jQuery 中 $(document).load() 监听的是 load 事件。

<span class="hljs-comment">// load</span>
<span class="hljs-variable language_">window</span>.<span class="hljs-property">onload</span> = <span class="hljs-keyword">function</span>() {};

<span class="hljs-comment">// DOMContentLoaded</span>
<span class="hljs-keyword">function</span> <span class="hljs-title function_">ready</span>(<span class="hljs-params">fn</span>) {
    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">addEventListener</span>) {
        <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">addEventListener</span>(
            <span class="hljs-string">'DOMContentLoaded'</span>,
            <span class="hljs-keyword">function</span>() {
                <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">removeEventListener</span>(<span class="hljs-string">'DOMContentLoaded'</span>, <span class="hljs-variable language_">arguments</span>.<span class="hljs-property">callee</span>, <span class="hljs-literal">false</span>);
                <span class="hljs-title function_">fn</span>();
            },
            <span class="hljs-literal">false</span>
        );
    }
    <span class="hljs-comment">// 如果 IE</span>
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">attachEvent</span>) {
        <span class="hljs-comment">// 确保当页面是在iframe中加载时,事件依旧会被安全触发</span>
        <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">attachEvent</span>(<span class="hljs-string">'onreadystatechange'</span>, <span class="hljs-keyword">function</span>() {
            <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">readyState</span> == <span class="hljs-string">'complete'</span>) {
                <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">detachEvent</span>(<span class="hljs-string">'onreadystatechange'</span>, <span class="hljs-variable language_">arguments</span>.<span class="hljs-property">callee</span>);
                <span class="hljs-title function_">fn</span>();
            }
        });
        <span class="hljs-comment">// 如果是 IE 且页面不在 iframe 中时,轮询调用 doScroll 方法检测DOM是否加载完毕</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">document</span>.<span class="hljs-property">documentElement</span>.<span class="hljs-property">doScroll</span> && <span class="hljs-keyword">typeof</span> <span class="hljs-variable language_">window</span>.<span class="hljs-property">frameElement</span> === <span class="hljs-string">'undefined'</span>) {
            <span class="hljs-keyword">try</span> {
                <span class="hljs-variable language_">document</span>.<span class="hljs-property">documentElement</span>.<span class="hljs-title function_">doScroll</span>(<span class="hljs-string">'left'</span>);
            } <span class="hljs-keyword">catch</span> (error) {
                <span class="hljs-keyword">return</span> <span class="hljs-built_in">setTimeout</span>(<span class="hljs-variable language_">arguments</span>.<span class="hljs-property">callee</span>, <span class="hljs-number">20</span>);
            }
            <span class="hljs-title function_">fn</span>();
        }
    }
}

readystatechange

documentreadyState属性来描述documentloading状态, readyState的改变会触发readystatechange事件.

  • loading: 文档文在加载
  • interactive: 文档结束加载并被解析, 但是图片, 样式, frame之类的子资源仍在加载
  • complete: 文档和子资源已经结束加载, 该状态表明将要触发loading事件.

因此, 我们同样可以使用该事件来判断dom的加载状态.

beforeunload

在浏览器窗口, 文档或器资源将要卸载时, 会触发beforeunload事件, 这个文档依然是可见的, 并且这个事件在这一刻是可以取消的.

unload

当文档或者一个资资源将要被卸载时, 在beforeunload,pagehide时间之后触发, 文档会处于一个特定状态:

  • 所有资源仍存在
  • 对于终端用户所有资源均不可见
  • 界面交互无效
  • 错误不会停止卸载文档的过程.
document.addEventListener("DOMContentLoaded", function (event) {
    console.log("初始DOM 加载并解析");
});
window.addEventListener("load", function (event) {
    console.log("window 所有资源加载完成");
});
document.onreadystatechange = function () {
    console.log(document.readyState)
    if (document.readyState === "complete") {
        console.log('初始DOM,加载解析完成')
    }
}
window.addEventListener("beforeunload", function (event) {
    console.log('即将关闭')
    event.returnValue = "\o/";
});
window.addEventListener('unload', function (event) {
    console.log('即将关闭1');
});

基本指标介绍及优化方案

首次绘制(FP,白屏)

FP (First Paint)是时间线上的第一个“时间点”,是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间,简而言之就是浏览器第一次发生变化的时间。

白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。首次绘制 FP 包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻。

当浏览器开始渲染页面,白屏触发,这时候你如果设置了背景颜色的话,就可以看到页面出现了背景色。

白屏会在页面加载之前触发,在这段时间里,不会呈现任何内容和信息给用户。虽然背景色会很快完成绘制,但是实际的内容和交互可能要花很长的时间去加载,因此,白屏时间过长,会让用户认为我们的页面不能用或可用性差。

前端常见性能指标

白屏示例图

影响白屏时间的因素: 网络、服务端性能、前端页面结构设计

计算:白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素

通常认为浏览器开始渲染<body>标签或者解析完<head>的时间是白屏结束的时间点。

<head>
...
<script>
    // 通常在head标签尾部时,打个标记,这个通常会视为白屏时间
    performance.mark("first paint time");
</script>
</head>
<body>
...
<script>
    // get the first paint time
    const fp = Math.ceil(performance.getEntriesByName('first paint time')[0].startTime);
</script>
</body>

一种比较简单的做法是在 body 标签之前获取当前时间 - performance.timing.navigationStart

<head>
<script>
  const fp = Date.now() - performance.timing.navigationStart
</script>
</head>

也可以使用其他的计算方法:白屏时间 = 页面开始展示的时间点 - 开始请求的时间点。

优化:可以使用骨架屏或Loading图来替换白屏,在白屏结束时移除相关DOM节点。

首次内容绘制(FCP,首屏)

FCP(First Contentful Paint),是指浏览器从响应用户输入网络地址,在页面首次绘制文本,图片(包括背景图)、非白色的 canvas 或者 SVG 才算做 FCP。
首屏渲染完成时间没有确切的标准。具体是哪个DOM元素绘制完成,或者是整个DOM绘制完成,由开发者定义。 实践中, 可以将页面评分最高的可见内容出现在屏幕上的时间作为FCP时间。

首屏决定了网页的用户体验,因为它会标记实际内容何时加载到页面中,而不仅仅是标记页面的变化状态。因为关注的是内容,所以该指标可以了解用户何时收到消耗性信息,比如文本,视觉效果等,这比通过背景改变或样式改变对用户体验进行评估更有用。

前端常见性能指标

首屏示例图

计算:首屏时间 = 地址输入网址后回车 - 浏览器第一屏渲染完成

关于首屏时间是否包含图片加载,通常有不同的说法。若认为不包含图片加载,则可在次元素后面加入 script 计算首屏完成的时间。(由于浏览器解析 HTML 是按照顺序解析的, 当解析到某个元素的时候, 认为首屏完成了),比如认为body渲染完成为首屏时间,则可如下计算:

<body> 
... 
<script> 
// 首屏时间
const fcp = Date.now() - performance.timing.navigationStart
</script>
</body>

如果认为首屏包含图片加载,可以统计首屏加载最慢的图片是否加载完成,加载完了,记录结束时间。由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。我们只需要监听首屏内所有的图片的 onload 事件,获取图片 onload 时间最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。

<body>
<div class="app-container">
    <img src="a.png" alt="前端常见性能指标" onload="imageLoaded()">
    <img src="b.png" alt="前端常见性能指标" onload="imageLoaded()">
    <img src="c.png" alt="前端常见性能指标" onload="imageLoaded()">
</div>
<script>
    // 根据首屏中的核心元素确定首屏时间
    performance.clearMarks("imageLoaded");
    performance.mark("imageLoaded");
    function imageLoaded() {
        performance.clearMarks("imageLoaded");
        performance.mark("imageLoaded");
    }
</script>
...
...
<script>
    // get the first screen loaded time
    const fmp = Math.ceil(performance.getEntriesByName('imageLoaded')[0].startTime);
</script>
</body>

可交互时间(TTI)

TTI(Time to Interactive),翻译为“可交互时间”表示网页第一次完全达到可交互状态的时间点。可交互状态指的是页面上的 UI 组件是可以交互的(可以响应按钮的点击或在文本框输入文字等),不仅如此,此时主线程已经达到“流畅”的程度,主线程的任务均不超过 50 毫秒。在一般的管理系统中,TTI 是一个很重要的指标。

关于 TTI 可以首先了解下谷歌提出的性能模型 RAIL:

前端常见性能指标

  1. 响应:输入延迟时间(从点按到绘制)小于 100 毫秒。用户点按按钮(例如打开导航)。
  1. 动画:每个帧的工作(从 JS 到绘制)完成时间小于 16 毫秒。用户滚动页面,拖动手指(例如,打开菜单)或看到动画。拖动时,应用的响应与手指位置有关(例如,拉动刷新、滑动轮播)。此指标仅适用于拖动的持续阶段,不适用于开始阶段。
  1. 空闲:主线程 JS 工作分成不大于 50 毫秒的块。用户没有与页面交互,但主线程应足够用于处理下一个用户输入。
  1. 加载:页面可以在 1000 毫秒内就绪。用户加载页面并看到关键路径内容。

我们可以通过 domContentLoadedEventEnd 来粗略的进行估算:

TTI:domContentLoadedEventEnd - navigationStart

谷歌实验室也提供了更加便捷准确的 api 包进行测算 tti-polyfil:

import ttiPolyfill from "./path/to/tti-polyfill.js";
ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
  // Use `tti` value in some way.
});

最大内容绘制(LCP)

LCP(Largest Contentful Paint,最大内容元素渲染)表示可视区“内容”最大的(绘制面积最大)可见元素开始出现在屏幕上的时间点。为了提供良好的用户体验, 网站应该努力在开始加载页面的前2.5s内进行最大内容渲染。

前端常见性能指标

LCP不会计算所有的元素, 它只关注:

  • img 元素
  • images中的svg元素
  • video元素
  • 通过url()函数加载背景图片的元素
  • 包含文本节点或者其他内联文本元素子级的块级元素

影响LCP较差的最常见原因是:

  • 服务器响应时间慢
  • 阻断渲染的js和css
  • 资源加载时间慢
  • 客户端渲染

改善LCP具体的措施有:

在过去,我们也有推荐的性能指标,如:FMP (First Meaningful Paint)SI (Speed Index)可以帮我们捕获更多的首次渲染之后的加载性能,但这些过于复杂,而且很难解释,也经常出错,没办法确定主要内容什么时候加载完。

根据W3C Web 性能工作组的讨论和 Google 的研究,发现度量页面主要内容的可见时间有一种更精准且简单的方法是查看 “绘制面积” 最大的元素何时开始渲染。

所谓绘制面积可以理解为每个元素在屏幕上的 “占地面积” ,如果元素延伸到屏幕外,或者元素被裁切了一部分,被裁切的部分不算入在内,只有真正显示在屏幕里的才算数。图片元素的面积计算方式稍微有点不同,因为可以通过 CSS 将图片扩大或缩小显示,也就是说,图片有两个面积:“渲染面积”与“真实面积”。在 LCP 的计算中,图片的绘制面积将获取较小的数值。例如:当“渲染面积”小于“真实面积”时,“绘制面积”为“渲染面积”,反之亦然。

页面在加载过程中,是线性的,元素是一个一个渲染到屏幕上的,而不是一瞬间全渲染到屏幕上,所以“渲染面积”最大的元素随时在发生变化。如果使用 PerformanceObserver 去捕获 LCP,会发现每当出现“渲染面积”更大的元素,就会捕获出一条新的性能条目。

如果元素被删除,LCP 算法将不再考虑该元素,如果被删除的元素刚好是 “绘制面积” 最大的元素,则使用新的 “绘制面积” 最大的元素创建一个新的性能条目。

该过程将持续到用户第一次滚动页面或第一次用户输入(鼠标点击,键盘按键等),也就是说,一旦用户与页面开始产生交互,则停止报告新的性能条目。

可以直接使用 PerformanceObserver 来捕获 LCP:

const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  const lcp = lastEntry.renderTime || lastEntry.loadTime;
  console.log("LCP:", lcp);
});
observer.observe({ entryTypes: ["largest-contentful-paint"] });

LCP 也不是完美的,也很容易出错,它会在用户进行交互后就停止捕获,可能会获取到错误的结果,如果有占据页面很大的轮播图也会产生问题会不断的更新 LCP

LCP 也有现成的计算工具库 web-vitals:

import { getLCP } from "web-vitals";

// Measure and log the current LCP value,
// any time it's ready to be reported.
getLCP(console.log);

首次可交互时间(FID)

FID(First Input Delay)即记录用户和页面进行首次交互操作所花费的时间,FID指标影响用户对页面交互性和响应性的第一影响。为了提供良好的用户体验,站点应该使首次输入延迟小于100毫秒。

FID发生在FCP和TTI之间,应为这个阶段虽然页面已经显示出部分的内容, 但尚不具备完全的可交互性. 这个阶段的用户交互往往有比较大的延迟。

浏览器接收到用户输入操作的时候,主线程正在忙于执行一个耗时比较长的任务,只有当这个任务执行完成以后, 浏览器才能响应用户的输入操作,他必须等待的时间就是此页面上该用户的FID值。

优化:

一方面,减少js的执行时间:

  • 缩小、压缩js文件
  • 延迟加载首屏不需要的js
  • 减少未使用的polyfill

另一方面,我们可以分解耗时任务:比如使用 web worker 独立运行耗时任务。

视觉稳定性(CLS)

布局偏移分值(Cumulative Layout Shift)是衡量页面整个生命周期中样式意外移动的指标。

页面内容位置意外变动是导致视觉不稳定的因素。其原因由于异步加载的资源或DOM元素动态添加而导致的。

CLS是通过测量页面的整个生命周期中发生的每个意外的样式移动的所有单独布局更改得分的总合。为了提供良好的用户体验, 网站应该努力让cls分数小于0.1。

计算:布局偏移分值 = 影响分数 * 距离分数

  • 影响分数:前一帧和当前帧所有不稳定元素的可见区域的并集(占视口总面积的部分)
  • 距离分数:是任何不稳定元素在框架中移动的最大距离/视口的最大尺寸

优化:

1. 不要使用无尺寸元素

图片和视频元素需要始终包含widthheight尺寸属性, 现代浏览器会根据widthheight设置图片的默认宽高比. 或者直接使用aspect-radio也可以提前指定宽高比:

img {
    aspect-ratio: attr(width) / attr(height);
}

对于响应式图片, 可以使用srcset定义图像, 使浏览器可以在图像之间进行选择, 以及每个图像的大小:

<img 
    width="1000" 
    height="1000"
    src="puppy-1000.jpg"
    srcset="puppy-1000.jpg 1000w,
            puppy-2000.jpg 2000w,
            puppy-3000.jpg 3000w"
    alt="ConardLi"
/>

2. 其他

  • 永远不要在现有内容之上插入内容, 除非是响应用户交互. 这能确保预期的布局变化
  • 宁可转换动画, 也不要转换触发布局变化的属性的动画.
  • 提前给广告位预留空间
  • 警惕字体变化, 使用font-display告诉浏览器默认使用系统字体进行渲染, 当自定义字体下载完成之后在进行替换
@font-face {
  font-family: 'Pacifico';
  font-style: normal;
  font-weight: 400;
  src: local('Pacifico Regular'), local('Pacifico-Regular'), url(https://fonts.gstatic.com/xxx.woff2) format('woff2');
  font-display: swap;
}

此外可以使用<link rel='preload'>提前加载字体文件。

指标获取

可以使用Chrome插件web-vitals-extension来获取一些指标。

Google提供了web-vitals来让我们便捷的获取CLS, FID, LCP这三个指标。

import {getCLS, getFID, getLCP} from 'web-vitals';

getCLS(console.log, true);
getFID(console.log); // Does not take a `reportAllChanges` param.
getLCP(console.log, true);

performance 介绍

performance 对象是专门用来用于性能监控的对象,内置了一些前端需要的性能参数。

performance.now()方法

performance.now() 返回 performance.navigationStart 至当前的毫秒数。performance.navigationStart 是下文将介绍到的可以说是浏览器访问最初的时间测量点。

performance.now(); // 24614164.599999994

performance.timing

前端常见性能指标

  1. navigationStart: 表示从上一个文档卸载结束时的 unix 时间戳,如果没有上一个文档,这个值将和 fetchStart 相等。
  2. unloadEventStart: 表示前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0。
  3. unloadEventEnd: 返回前一个页面 unload 时间绑定的回掉函数执行完毕的时间戳。
  4. redirectStart: 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0。
  5. redirectEnd: 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0。
  6. fetchStart: 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前。
  7. domainLookupStart/domainLookupEnd: DNS 域名查询开始/结束的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
  8. connectStart: HTTP(TCP)开始/重新 建立连接的时间,如果是持久连接,则与 fetchStart 值相等。
  9. connectEnd: HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等。
  10. secureConnectionStart: HTTPS 连接开始的时间,如果不是安全连接,则值为 0。
  11. requestStart: HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存。
  12. responseStart: HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存。
  13. responseEnd: HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存。
  14. domLoading: 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件。
  15. domInteractive: 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件,注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源。
  16. domContentLoadedEventStart: DOM 解析完成后,网页内资源加载开始的时间,在 DOMContentLoaded 事件抛出前发生。
  17. domContentLoadedEventEnd: DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)。
  18. domComplete: DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。
  19. loadEventStart: load 事件发送给文档,也即 load 回调函数开始执行的时间。
  20. loadEventEnd: load 事件的回调函数执行完毕的时间。
// 计算加载时间
function getPerformanceTiming() {
    var t = performance.timing;
    var times = {};
    // 页面加载完成的时间,用户等待页面可用的时间
    times.loadPage = t.loadEventEnd - t.navigationStart;
    // 计算dom渲染耗时
    times.domTimes = timing.domComplete - timing.domLoading
    // 解析 DOM 树结构的时间
    times.domReady = t.domComplete - t.responseEnd;
    // 重定向的时间
    times.redirect = t.redirectEnd - t.redirectStart;
    // DNS 查询时间
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
    // 读取页面第一个字节的时间
    times.ttfb = t.responseStart - t.navigationStart;
    // 资源请求加载完成的时间
    times.request = t.responseEnd - t.requestStart;
    // 执行 onload 回调函数的时间
    times.loadEvent = t.loadEventEnd - t.loadEventStart;
    // DNS 缓存时间
    times.appcache = t.domainLookupStart - t.fetchStart;
    // 卸载页面的时间
    times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;
    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;
    return times;
}

performance.navigation

redirectCount: 0 // 页面经过了多少次重定向

type: 0

  • 0 表示正常进入页面;
  • 1 表示通过 window.location.reload() 刷新页面;
  • 2 表示通过浏览器前进后退进入页面;
  • 255 表示其它方式

performance.memory

  • jsHeapSizeLimit: 内存大小限制
  • totalJSHeapSize: 可使用的内存
  • usedJSHeapSize: JS 对象占用的内存

performance.getEntries()方法

浏览器获取网页时,会对网页中每一个对象(脚本文件、样式表、图片文件等等)发出一个 HTTP/HTTPS 请求。performance.getEntries() 方法以数组形式,返回一个 PerformanceEntry 列表,这些请求的时间统计信息,有多少个请求,返回数组就会有多少个成员。

前端常见性能指标

加载中...
加载中...