文档流加载周期
-
DOMContentLoaded 是指页面元素加载完毕,但是一些资源比如图片还无法看到,但是这个时候页面是可以正常交互的,比如滚动,输入字符等。 jQuery 中经常使用的 $(document).ready() 其实监听的就是 DOMContentLoaded 事件。
-
load 是指页面上所有的资源(图片,音频,视频等)加载完成。jQuery 中 $(document).load() 监听的是 load 事件。
// load
window.onload = function() {};
// DOMContentLoaded
function ready(fn) {
if (document.addEventListener) {
document.addEventListener(
'DOMContentLoaded',
function() {
document.removeEventListener('DOMContentLoaded', arguments.callee, false);
fn();
},
false
);
}
// 如果 IE
else if (document.attachEvent) {
// 确保当页面是在iframe中加载时,事件依旧会被安全触发
document.attachEvent('onreadystatechange', function() {
if (document.readyState == 'complete') {
document.detachEvent('onreadystatechange', arguments.callee);
fn();
}
});
// 如果是 IE 且页面不在 iframe 中时,轮询调用 doScroll 方法检测DOM是否加载完毕
if (document.documentElement.doScroll && typeof window.frameElement === 'undefined') {
try {
document.documentElement.doScroll('left');
} catch (error) {
return setTimeout(arguments.callee, 20);
}
fn();
}
}
}
复制代码
- readystatechange
document
有readyState
属性来描述document
的loading
状态, 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');
});
复制代码
Performance API
Performance 接口可以获取到当前页面与性能相关的信息。
- Performance.timing
在 chrome 中查看 performance.timing 对象:
PerformanceTiming {
connectEnd: 1568364862807
connectStart: 1568364862530
domComplete: 1568364863751
domContentLoadedEventEnd: 1568364863699
domContentLoadedEventStart: 1568364863698
domInteractive: 1568364863694
domLoading: 1568364863438
domainLookupEnd: 1568364862529
domainLookupStart: 1568364862529
fetchStart: 1568364862529
loadEventEnd: 1568364863785
loadEventStart: 1568364863751
navigationStart: 1568364862499
redirectEnd: 0
redirectStart: 0
requestStart: 1568364862807
responseEnd: 1568364863437
responseStart: 1568364863434
secureConnectionStart: 1568364862530
unloadEventEnd: 0
unloadEventStart: 0
}
复制代码
对应浏览器的状态如下:
左边红线代表了网络传输层的过程, 右边红线代表了服务器传输回字节后浏览的各种事件状态, 这个阶段包含了浏览器对文档的解析, DOM 树构建, 布局, 绘制等.
- navigationStart: 表示从上一个文档卸载结束时的 unix 时间戳,如果没有上一个文档,这个值将和 fetchStart 相等。
- unloadEventStart: 表示前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0。
- unloadEventEnd: 返回前一个页面 unload 时间绑定的回掉函数执行完毕的时间戳。
- redirectStart: 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0。
- redirectEnd: 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0。
- fetchStart: 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前。
- domainLookupStart/domainLookupEnd: DNS 域名查询开始/结束的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
- connectStart: HTTP(TCP)开始/重新 建立连接的时间,如果是持久连接,则与 fetchStart 值相等。
- connectEnd: HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等。
- secureConnectionStart: HTTPS 连接开始的时间,如果不是安全连接,则值为 0。
- requestStart: HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存。
- responseStart: HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存。
- responseEnd: HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存。
- domLoading: 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件。
- domInteractive: 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件,注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源。
- domContentLoadedEventStart: DOM 解析完成后,网页内资源加载开始的时间,在 DOMContentLoaded 事件抛出前发生。
- domContentLoadedEventEnd: DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)。
- domComplete: DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。
- loadEventStart: load 事件发送给文档,也即 load 回调函数开始执行的时间。
- loadEventEnd: load 事件的回调函数执行完毕的时间。
计算加载时间:
// 计算加载时间
function getPerformanceTiming() {
var t = performance.timing;
var times = {};
// 页面加载完成的时间,用户等待页面可用的时间
times.loadPage = t.loadEventEnd - t.navigationStart;
// 解析 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 对象占用的内存
Core Web Vitals
Core Web Vitals
是应用于所有的web页面的Web Vitals
的子集, 他们将在所有谷歌提供的性能测试工具中进行显示, 每个Core Web Vitals
代表用户体验的一个不同方面, 在该领域是可衡量的, 并反映了以用户为中心的关键结果的真实体验.
网页核心的性能指标是随着时间的推移而不断的演变的. 在2020年, 主要关注用户体验的三个方面: 加载, 交互性和视觉稳定性.
Largest Contentful Paint (LCP)
: 衡量加载体验, 为了提供良好的用户体验, LCP应该在页面首次开始后的2.5s内发生.First Input Delay(FID)
: 衡量可交互性, 页面的FID应该小于100msCumulative Layout Shift(CLS)
: 衡量视觉稳定性的指标, 页面的CLS应该小于0.1
LCP: 最大内容元素渲染
衡量加载体验的指标.
最早我们使用load
, DOMContentLoaded
事件, 但是他们与实际上用户屏幕上的内容是不一定对应的.
之后我们尝试使用以用户为中心的更新性能指标, 例如First Content Paint(FCP)
, 它只能捕获加载体验的最开始. 如果页面开始是一个loading
动画, 这个指标就不准确了.
后来, 业界开始建议使用First Meaningful Paint(FMP)
和Speed Index(SI)
(都可以在Lighthouse
中获取到), 但这些指标非常复杂, 难以解释, 误报率较高.
定义
Largest Contentful Paint (LCP)
用于衡量标准报告视口内可见的最大内容元素的渲染时间. 为了提供良好的用户体验, 网站应该努力在开始加载页面的前2.5s内进行最大内容渲染.
关注的元素
LCP不会计算所有的元素, 它只关注:
- img 元素
- images中的svg元素
- video元素
- 通过
url()
函数加载背景图片的元素 - 包含文本节点或者其他内联文本元素子级的块级元素
计算
我们需要关注的是页面上最大的元素即绘制面积最大的元素.
所谓的绘制面积可以理解为每个元素在屏幕上的”占地面积”. 如果元素延伸到屏幕外, 或者被裁切了一部分, 这部分就不会计算入内.
图片元素的面积计算稍有点不同, 因为可以通过CSS将图片扩大或者缩小显示, 也就是说, 图片有渲染面积和真实面积的区别. 在LCP的计算中, 图片的绘制面积将取两者中比较小的那个. 比如当渲染面积小于真实面积的时候, 取渲染面积, 反之亦然.
页面在加载过程中是线性的, 元素是一次渲染, 而不是瞬间渲染, 所以渲染面积最大的元素随时在发生变化.
如果元素被删除, LCP算法就不再考虑这个元素, 如果被删除的元素刚好是”绘制面积”最大的元素, 则使用新的”绘制面积”最大的元素创建一个新的性能条目.
这个过程会只需要用户第一次滚动页面或者第一次用户输入(鼠标点击, 键盘按键等), 也就是说, 一旦用户与页面开始产生交互, 就停止报告新的性能指标.
优化
影响LCP较差的最常见原因是:
- 服务器响应时间慢
- 阻断渲染的js和css
- 资源加载时间慢
- 客户端渲染
所以我们从上面的角度去改善LCP.
具体的措施有:
- 缓存HTML离线页面, 缓存页面资源, 减少浏览器对资源的请求
- 减小资源组算渲染, 对CSS/JS进行压缩, 合并, 级联, 内联等
- 对图片进行优化, 转换格式为JPG, webp等, 降低图片大小, 加载请求的速度
- 对HTML重写, 压缩空格, 去除注释
- 使用preconnet尽快建立链接, 使用dns-prefect尽快进行DNS查找
- 使用CDN加载请求速度
- 使用Gzip压缩页面
- 使用sw缓存资源和请求等
FID: 首次可交互事件
First Contentful Paint(FCP)可以衡量网站加载速度, 但是绘制的速度只是一般部分, 同样重要的是用户尝试与这些像素进行交互的时候网站的反应速度.
定义
FID(First Input Delay), 即记录用户和页面进行首次交互操作所花费的时间, FID指标影响用户对页面交互性和响应性的第一影响. 为了提供良好的用户体验, 站点应该使首次输入延迟小于100毫秒
FID发生在FCP和TTI之间, 应为这个阶段虽然页面已经显示出部分的内容, 但尚不具备完全的可交互性. 这个阶段的用户交互往往有比较大的延迟.
浏览器接收到用户输入操作的时候, 主线程正在忙于执行一个耗时比较长的任务, 只有当这个任务执行完成以后, 浏览器才能响应用户的输入操作. 他必须等待的时间就是此页面上该用户的FID值.
优化
一方面, 我们可以向上面一样, 减少js的执行时间:
- 缩小压缩js文件
- 延迟加载首屏不需要的js
- 减少未使用的polyfill
另一方面, 我们可以分解耗时任务:
- 使用 web worker 独立运行耗时任务.
CLS: 视觉稳定性
页面内容的意外移动是由于异步加载的资源或将DOM元素动态添加到现有内容上方的页面而发生的.
定义
Cumulative Layout Shift (CLS), 会测量页面的整个生命周期中发生的每个意外的样式移动的所有单独布局更改得分的总合. 布局的移动可能发生在可见元素从当前一帧到下一帧改变位置的任何时候. 为了提供良好的用户体验, 网站应该努力让cls分数小于0.1.
计算
布局偏移分值, 用于计算元素移动的指标. 是目标元素的两个指标(影响分数, 距离分数)的乘积:
布局偏移分值 = 影响分数 * 距离分数
复制代码
影响分数, 值的是前一帧和当前帧所有不稳定元素的可见区域的并集(站视口总面积的部分).
比如这里, 一个元素在上一帧中占据视口的一般, 在下一帧中下移25%. 红色的曲线就是两个帧中元素的可见区域的并集. 这种情况下, 影响分值为0.75
距离分数, 是任何不稳定元素在框架中移动的最大距离/视口的最大尺寸
:
在这个例子中, 最大视口尺寸是高度, 不稳定元素移动了视口高度的25%, 这使得距离分数为0.25.
因此, 在这个例子中, 最终的CLS得分为: 0.75 * 0.25 = 0.1875
.
优化
1. 不要使用无尺寸元素
图片和视频元素需要始终包含width
和height
尺寸属性, 现代浏览器会根据width
和height
设置图片的默认宽高比. 或者直接使用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'>
提前加载字体文件.
获取参数
方式1: Google提供了web-vitals
来让我们便捷的调用这三个参数
import {getCLS, getFID, getLCP} from 'web-vitals';
getCLS(console.log, true);
getFID(console.log); // Does not take a `reportAllChanges` param.
getLCP(console.log, true);
复制代码
其回到函数中提供了三个指标:
- name: 指标的名称
- id: 本地分析的id
- delta: 当前值和上次获取值的差值
可以结合google analytics
来上报指标:
import {getCLS, getFID, getLCP} from 'web-vitals';
function sendToGoogleAnalytics({name, delta, id}) {
ga('send', 'event', {
eventCategory: 'Web Vitals',
eventAction: name,
eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
eventLabel: id,
nonInteraction: true,
});
}
getCLS(sendToGoogleAnalytics);
getFID(sendToGoogleAnalytics);
getLCP(sendToGoogleAnalytics);
复制代码
方式2: 使用Chrome插件
如果你不想在程序中计算, 还可以使用Chrome
插件web-vitals-extension
来获取这些指标.
其他常见性能指标
FP: 首次绘制事件
FP(First Paint, 首次绘制时间), 是时间线上的第一个”时间点”, 它代表浏览器第一次向屏幕传输像素的时间, 也就是页面在屏幕上首次发生视觉变化的时间.
也就是我们所说的白屏时间(浏览器从响应用户输入网址到浏览器开始显示内容的时间).
白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素
复制代码
影响白屏时间的因素: 网络, 服务端性能, 前端页面结构设计
通常认为浏览器开始渲染<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>
复制代码
FCP: 首次内容绘制事件
FCP(First Contentful Paint, 首次内容绘制), 代表浏览器第一次向屏幕绘制”内容”.
只有首次绘制文本, 图片, 非白色的canvas或者SVG的时候才被算作FCP.
也就是我们所说的首屏时间(浏览器从响应用户输入网络地址到首屏内容渲染完成的时间):
首屏时间 = 地址输入网址后回车 - 浏览器第一屏渲染完成
复制代码
关于首屏时间是否包含图片加载, 通常有不同的说法, 但那无关紧要.
计算首屏时间常用的方法有:
- 首屏模块标签标记法
由于浏览器解析 HTML 是按照顺序解析的, 当解析到某个元素的时候, 认为首屏完成了, 就在次元素后面加入 script 计算首屏完成的时间.
// 首屏屏结束时间
window.firstPaint = Date.now();
// 首屏时间
console.log(firstPaint - performance.timing.navigationStart);
复制代码
- 统计首屏内加载最慢的图片/iframe
通常首屏内容中加载最慢的就是图片或者 iframe 资源,因此可以理解为当图片或者 iframe 都加载出来了,首屏肯定已经完成了。
由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。我们只需要监听首屏内所有的图片的 onload 事件,获取图片 onload 时间最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。
<body>
<div class="app-container">
<img src="a.png" onload="heroImageLoaded()">
<img src="b.png" onload="heroImageLoaded()">
<img src="c.png" onload="heroImageLoaded()">
</div>
<script>
// 根据首屏中的核心元素确定首屏时间
performance.clearMarks("hero img displayed");
performance.mark("hero img displayed");
function heroImageLoaded() {
performance.clearMarks("hero img displayed");
performance.mark("hero img displayed");
}
</script>
...
...
<script>
// get the first screen loaded time
const fmp = Math.ceil(performance.getEntriesByName('hero img displayed')[0].startTime);
</script>
</body>
复制代码
:::tip 注意
注意: FP与FCP这两个指标之间的主要区别在于: FP是当浏览器开始绘制内容到屏幕上的时候, 只要在视觉上开始发生变化, 无论是什么内容触发的视觉变化, 这一刻的时间点, 就叫做FP.
相比之下, FCP指的是浏览器首次绘制来自DOM的内容, 例如文本, 图片, SVG, CANVAS等元素. 这个时间点叫做FCP
FP和FCP的时间点可能相同, 也可能是先FP, 后FCP.
:::
FMP: 首次主要内容绘制事件
FirstMeaningfulPaint, 首次主要内容渲染时间, 目前没有标准化的定义方式. 实践中, 可以将页面评分最高的可见内容出现在屏幕上的时间作为FCP时间
统计方式: 我们可以使用Mutation Observer
观察页面加载的前30s中页面节点的变化, 将新增/移除的节点加入/移除intersection Observer
, 这样可以得到页面元素的可见时间点及元素与可视区域的交叉信息. 根据元素的类型进行权重取值, 然后按可见度, 交叉区域面积, 权重值之间的乘积作为元素评分. 根据上面得到的信息, 以时间点为X周, 该时间点可见元素的评分总和为Y周, 取最高点对应的最小事件为页面主要内容出现在屏幕上的时间点.
目前没有统一的逻辑, 阿里有一个标准为最高可见增量元素, 采用深度优先遍历, 详情可以参考这里
LT: 长任务
当一个任务执行时间超过50ms
时消耗到的任务. 50ms阈值是从RAIL模型总结出来的结论,这个是google研究用户感知得出的结论,类似永华的感知/耐心的阈值,超过这个阈值的任务,用户会感知到页面的卡顿.
计算
// Jartto's Demo
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// TODO...
console.log(entry);
}
});
observer.observe({entryTypes: ['longtask']});
复制代码
TTI: 页面可交互时间
TTI(Time To Internative): 从页面开始到它的主要子资源加载到能够快速地响应用户输入的时间. (没有耗时长任务)
计算
我们可以通过domContentLoadedEventEnd
进行一个粗略的估算:
TTI: domContentLoadedEventEnd - navigationStart,
复制代码
如果你需要更精细的计算即如果, 可以通过Google
提供的tti-polyfill
来进行数据获取:
import ttiPolyfill from 'tti-polyfill';
ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
// Use `tti` value in some way.
});
复制代码
TBT: 页面阻塞总时长
TBT (Total Blocking Time) 页面阻塞总时长: TBT汇总所有加载过程中阻塞用户操作的时长,在FCP和TTI之间任何long task中阻塞部分都会被汇总.