前端性能优化总结
什么是 Web 性能优化
前端性能优化的目的: 让用户访问网站开始到页面完整展示出来的过程中, 通过各种优化策略和优化方法, 让页面加载的更快, 让用户操作响应更加及时, 给用户带来更好的用户体验
为什么需要性能优化
研究表明, 网页性能差直接加速产品的衰败, 也影响网站收入(广告), 因此我们需要提升 Web 性能从而提升用户体验, 公司营收等
优化步骤
以下笔者来详述所有常见的优化手段
一、图片优化
图片格式选取
JPEG(JPG/JPE): 有损压缩格式, 不支持透明度, 体积占用不大, 颜色细节质量不高, 颜色丰富, 通常网页大图(bannber / 轮播)等需要使用
PNG(PNG-8/PNG24): 无损压缩格式, 体积占用大, 细节表现好, 通常用于图标 / LOGO 等
GIF: 不支持半透明, 支持全透明, 通常用于动画图标
WEBP: Google 开源的图像格式, 无损的 WEBP 比 PNG 小 26%, 有损 WEBP 比 JPEG 小 25-34%, 比 GIF 有更好的动画, 但是兼容性不好, 需要做 Hack 处理
图片压缩
在实际的使用中, 必须对图片进行压缩, 常用以下工具进行压缩, 可以在本地压缩后上传至 CDN, 也可以在Node
服务端使用在线处理:
- 在线压缩 TinyPng(TinyJpg)
- JPG 压缩工具: Jpegtran
- PNG 压缩工具: node-pngquant-native
- GIF 压缩工具: Gifsicle
响应式图片
不同的网络环境, 应该加载不同尺寸和像素的图片, 通过请求不同的 URL 参数
httP://img.xxx.com/images/q100x100/c2exas....
对应的图片是 100x100
实现方式:
-
通过 JS 读取窗口大小, 选择合适的图片
-
通过媒体查询
@media screen and (max-width: 640px) {
.img_640 {
width: 640px;
}
}
- 通过 H5 的新属性
srcset
<img srcset="img-320w" />
逐步加载图片
- 使用统一占位符
-
使用
LQIP
(低质量图像占位符)安装:
npm install lqip
, 使用lqip-loader
来引入 -
使用
SQIP
(基于 SVG 的图像占位符)安装:
npm install sqip
图片降级方案
- Web Font 代替图片
- 使用 Data URI 代替图片
- 采用雪碧图(image spriting)
二、HTML 优化
-
减少 HTML 的嵌套, 减少 DOM 的节点数
-
压缩 HTML,删除不必要的字符
可以使用构建工具的插件html-webpack-plugin
- HTM 的结构优化
CSS
样式尽量放页面的头部, JS引用放在HTML底部
CSS 加载不会阻塞 DOM Tree 的解析, 但是会阻塞 DOM Tree 的渲染, 也会阻塞后面 JS 的执行。因此 body 元素之前, 可以确保在文档中解析了所有 CSS 样式, 从而减少了浏览器必须重排文档的次数。如果放在底部, 就需要等待最后一个 css 文件下载完成, 出现白屏,影响用户体验
JS 放在底部是防止加载、解析、执行对阻塞页面后续元素的正常渲染
- 设置 favicon.ico
网站不设置 favicon.ico,控制台会报错,设置的优点是更便于用户对品牌的记忆
- 增加网页的骨架屏
三、CSS 优化
- 避免使用通配符*和类正则属性选择器
- 避免使用类的多层级和装饰写法: div#elem.view ul li span{}
- 避免使用占用过多 CPU 和内存的属性:
text-indent: -9999px
- 关注可继承的 CSS 属性, 避免重复定义相同的属性
- 避免使用 table 布局/float 布局, 一个 td 会导致整个回流
- 使用外链 css(CDN 部署),避免使用@import(阻塞 css 文件加载)
- CSS 文件压缩
- 字体部署在 CDN, 或者将字体以 base64 保存在 css 中并通过 localstorage 缓存
- Google 使用国内托管
- CSS 复杂动画应该尽量将该元素脱离文档流, 否则会引起元素以后的所有元素频繁的回流
- 合理开启 GPU 加速(opacity/will-change/transform/filters), 过多的使用会导致内存占用大, 抗锯齿无效
四、JS 优化
JS 代码优化
- JS 文件放在<body>底部
- 使用节流和防抖
- 使用事件委托
- 避免使用 eval, 太耗性能
- 避免函数嵌套定义, 会导致多次预编译
- JS 函数的参数类型尽量一致,V8 会调用
turboFan
进行机器码编译优化
JS 的动画优化
- 避免添加大量 JS 动画
- 使用
requestAnimationFrame
代替setTimeout
和setInterval
requestAnimationFrame
告诉浏览器在下次重绘前执行, 而setTimeout
和setInterval
无法保证回调的执行时机
- 动画最好使用 canvas
- 尽量使用 CSS3 动画方案
JS 对 DOM 的操作优化
- 防止频繁的操作 DOM, 尽量批量化操作
- 将 DOM 离线再进行大量操作
- 避免触发同步布局事件
(offset | client | scroll)(Top | Left | Width | Height 的获取都应该缓存起来 |
五、Webpack 优化
- 依赖包优化(选用相同功能的小库)
- 缩小文件查询时间: (resolve.extension / resolve.mainFields / resolve.modules / resolve.alias)
- Loader 优化(babel 的 cacheDirectory:true / include / exclude / module.noParse)
- HappyPack 多进程打包 / hardSourceWebpackPlugin 设置中间模块缓存 / TerserWebpackPlugin / ingorePlugin / Dll 动态链接库 & DllReference
- TreeShaking / Scope hosting
- 压缩 CSS(optimize-css-assets-webpack-plugin / mini-css-extract-plugin)
- 分包按需加载(splitChunks.cacheGroup)
- long term cache (固化 js 的 chunkhash / 固化 css 的 contenthash / 固化 chunkId(optimize.chunkIds: ‘hashd’) / 固化 moduleIds(optimize.moduleIds:’name’) / 按需加载模块使用魔法字符串’webpackChunkName’ / 提取 webpack 的 runtime 代码)
六、使用缓存
- memory cache
- service worker
- disk cache
- push cache(一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放, 同一个 h2 连接可以共享)
七、浏览器的渲染过程
- 浏览器解析 HTML,生成 DOM Tree
- 浏览器解析 CSS, 生成 CSSOM Tree
- 浏览器将 DOM Tree 和 CSSOM Tree 合成渲染树
- 布局: 根据生成的 Render Tree, 进行回流, 计算出每个节点的几何位置
- 绘制: 根据渲染树和回流得到几何信息,得到每个节点的绝对像素,并生成图层
- CPU 将默认图层和复合图层输入到 GPU 进行合成, 最终的到了页面并展示
八、渲染优化
-
服务端渲染
包括后端同步渲染、同构直出、BigPipe
-
客户端渲染
JS 渲染:静态化、前后端分离、单页面应用 WebApp: React、 Vue、Angular、PWA 原生 APP: IOS、Android HybridApp: PhoneGap、Appcan 跨平台开发: RN、Flutter、 小程序
预渲染
同构方案集合 CSR 与 SSR 的优点,可以适用于大部分业务场景。但由于在同构的系统架构中,连接前后端的 Node 中间层处于核心链路,系统可用性的瓶颈就依赖于 Node ,一旦作为短板的 Node 挂了,整个服务都不可用
一般的场景,使用预渲染即可, 使用 webpack 插件prerender-spa-plugin
缺点:
- 预渲染只是快照页面, 不适合频繁变动页面
- 设置路由多, 构建时间增长
同构直出
降低首屏渲染时间, 利于 SEO, 直接上线 2 个版本, 利于灾备
- next.js 服务端渲染 React 组件框架
- gatsbyjs: 服务端 React 渲染框架
- nuxt.js 服务端渲染 Vue 框架
关于渲染的技术选型
-
依赖业务形式: 根据业务情况, 选择最佳的业务方案
-
依赖团队规模: 创业初期选择同步直出 JSP, 后面团队变大可以使用同构直出
Node server
, 富余人力用 PWA 等等 -
依赖技术水平: 适合公司的技术水平, 选择合适的技术方案
九、加载优化
- 懒加载
对长网页延迟加载特定元素(图片、JS/CSS),也可以是 JS 的特定函数和方法,优点是减少当前屏无效资源的加载
- 预加载
让浏览器预加载某些资源(图片、js、css、模板),提前加载到本地,后面使用直接从缓存中获取,优点是减少用户后续加载资源的等待时间
方式一:
<img src="https://xxxx" style="display:none" />
方式二:
const img = new Image();
img.src = "https://xxxx";
方式三:
<!-- 当前页需要的资源 as最高优先级,没有as被看做异步请求 -->
<link rel="preload" href="style.css" as="style" />
<!-- 其他页需要的资源 -->
<link rel="prefetch" href="image.png" />
<!-- 预解析跨域的DNS,避免用来解析当前站 -->
<link rel="dns-prefetch" href="https://xxx.com" />
<!-- 预先建立与服务器的连接 -->
<link rel="preconnect" href="https://xxx.com" crossorigin />
- 预渲染
优点:
懒加载组件出来之前, 用户需要时间等待完成; 还有一种预加载的方式是提前渲染, 渲染好后隐藏起来, 用的时候直接展示
实现方式:
<link rel="prerender" href="https://xxx.com" />
- 按需加载
可以分为常规按需加载
(js 或者其他脚本)、不同 App 按需加载(js-sdk 脚本)、 不同设备按需加载(pc 和 h5 样式)、不同分辨率按需加载(css 媒体查询)
十、接口优化
- 接口合并
一个页面很多业务接口和依赖的第三方接口统一起来,在部署在集群的接口上统一调用,减少页面请求
- 接口上 CDN
这是基于接口的性能考虑,把不需要实时更新的接口同步到 CDN,等接口内容变更之后自动同步到 CDN 集群上。如果一定时间内未请求到数据,回源站接口请求
- 接口域名上 CDN
增强可用性,稳定性
- 接口降级
电商大促中,核心接口进行降级备用基础接口进行业务实现。例如推荐接口,大促可以直接用运营的编辑数据。防止接口无法使用时,备用垫底备份数据
- 接口监控
不是指服务端的TP99
,是指用户实际情况成功和失败的情况, 包括弱网、超时、网络异常、网络切换等
- 接口缓存
包括 ajax 缓存、本地缓存(localstorage)、重发请求(网络切换)
十一、WebView 优化
IOS 的 webveiw 分为UIWebview
和WKWebview
, 后者性能更优,内存占用较前者低,加载速度快,可以直接与 JS 互调函数, 而UIWebview
需要第三方库来完善; WKWebview
的缺点是不支持自动注入cookie
,不支持 POST 参数
Android 的系统 webview 分为Webkit Webview
和chromium Webview
(更优秀), 第三方的 webview 主要有X5内核
,速度更快, 兼容性更好, 国内各种手机厂商的碎片化支持更好, 视频播放更加强大
因此 IOS 选用WKWebview
和 Android 使用X5内核
- 使用全局 Webview 优化
APP 启动, 默认不初始化浏览器内核, 当创建实例时才启动内核, 大概有 70-700ms 延迟。客户端刚启动就初始化全局 webview, 需要使用时,直接加载内容;但是额外会消耗一些内存
- URL 预加载
准备和请求页面同步进行,URL load 和动画并行加载
- 滚动条使用体验
模拟 WIFI 下页面加载过程, 让用户感觉变快
- JS-SDK 的优化
一般来说, 常用有三种方式可以调用 nativeApi, 包括上下文注入
、弹窗拦截
、URL Scheme
等
现在可直接使用webkit
直接调用
- H5 离线包方案
首先加载全局包->判断本地是否安装->如果安装了直接解包到内存->如果未安装去线上比对后下载再解包到内存->webview 加载资源时候直接读取内存中的数据->内存中存在直接返回->否则去线上地址取
十一、混合式 APP(RN/Weex)
- RN 的实现方式
技术选型: React 技术全家桶可以选用 RN
- Flutter 的实现方式
)
学习曲线: 相对比 RN 高,重新学习 Dart 语言
性能: Native 性能最好,直接和 Skia(C/C++)
引擎通信,没有 JS Bridge
层
选型建议: 考虑性能, 业务面向多终端, APP 团队人员多
十二、CDN 优化
优点
避免 Ddos 攻击、高可用性处理高流量和负载、节省流量
HTTP 请求流程说明
1、用户在浏览器输入要访问的网站域名,向本地 DNS 发起域名解析请求。
2、域名解析的请求被发往网站授权 DNS 服务器。
3、网站 DNS 服务器解析发现域名已经 CNAME 到了 www.example.com.c.cdnhwc1.com。
4、请求被指向 CDN 服务。
5、CDN 对域名进行智能解析,将响应速度最快的 CDN 节点 IP 地址返回给本地 DNS。
6、用户获取响应速度最快的 CDN 节点 IP 地址。
7、浏览器在得到速度最快节点的 IP 地址以后,向 CDN 节点发出访问请求。
8、CDN 节点将用户所需资源返回给用户。
CDN 缓存
常用建议:
- HTML:3 分钟
- JS/CSS: 10 分钟、1 天、30 天
CDN 的Nginx
中通过设置 expires 字段的时长
CDN 灰度发布
不会区域/地区部分运营商优先发布静态资源, 验证通过后, 再进行全量发布
通过设置特殊 VIP 解析至要灰度的城市/运营商
CDN 备战
如果是大促, 增加机房带宽
/ 增加运营商流量
/ CDN 应用缓存时间由 10 分钟设置成 1 小时, 大促后恢复
十三、DNS 优化
DNS查询
访问远程服务的时候,不会直接使用服务的出口 IP,而是使用域名。DNS 是应用层协议,事实上他是为其他应用层协议工作的,包括不限于 HTTP 和 SMTP 以及 FTP,用于将用户提供的主机名解析为 ip 地址
DNS 获取的流程主要分为以下几个步骤:
-
浏览器缓存
:当用户通过浏览器访问某域名时,浏览器首先会在自己的缓存中查找是否有该域名对应的 IP 地址(若曾经访问过该域名且没有清空缓存便存在) -
系统缓存
:当浏览器缓存中无域名对应 IP 则会自动检查用户计算机系统 Hosts 文件 DNS 缓存是否有该域名对应 IP; -
路由器缓存
:当浏览器及系统缓存中均无域名对应 IP 则进入路由器缓存中检查,以上三步均为客服端的 DNS 缓存; -
ISP(互联网服务提供商)DNS 缓存
:当在用户客服端查找不到域名对应 IP 地址,则将进入 ISP DNS 缓存中进行查询。比如你用的是电信的网络,则会进入电信的 DNS 缓存服务器中进行查找; -
根域名服务器
:当以上均未完成,则进入根服务器进行查询。全球仅有 13 台根域名服务器,1 个主根域名服务器,其余 12 为辅根域名服务器。根域名收到请求后会查看区域文件记录,若无则将其管辖范围内顶级域名(如.com)服务器 IP 告诉本地 DNS 服务器; -
顶级域名服务器
:顶级域名服务器收到请求后查看区域文件记录,若无则将其管辖范围内主域名服务器的 IP 地址告诉本地 DNS 服务器; -
主域名服务器
:主域名服务器接受到请求后查询自己的缓存,如果没有则进入下一级域名服务器进行查找,并重复该步骤直至找到正确纪录; -
保存结果至缓存
:本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时将该结果反馈给客户端,客户端通过这个 IP 地址与 web 服务器建立链接。
::: tip 这里我们需要了解的是
- 首先,DNS 解析流程可能会很长,耗时很高,所以整个 DNS 服务,包括客户端都会有缓存机制,这个作为前端不好涉入;
- 其次,在 DNS 解析上,前端还是可以通过浏览器提供的其他手段来“加速”的。
:::
DNS Prefetch 就是浏览器提供给我们的一个 API。它是 Resource Hint 的一部分。它可以告诉浏览器:过会我就可能要去 yourwebsite.com 上下载一个资源啦,帮我先解析一下域名吧。这样之后用户点击某个按钮,触发了 yourwebsite.com 域名下的远程请求时,就略去了 DNS 解析的步骤。使用方式很简单
<link rel="dns-prefetch" href="//yourwebsite.com" />
DNS查询优化-预先建立连接
我们知道,建立连接不仅需要 DNS 查询,还需要进行 TCP 协议握手,有些还会有 TLS/SSL 协议,这些都会导致连接的耗时。使用 Preconnect[3] 可以帮助你告诉浏览器:“我有一些资源会用到某个源(origin),你可以帮我预先建立连接”
根据规范,当你使用 Preconnect 时,浏览器大致做了如下处理:
- 首先,解析 Preconnect 的 url;
- 其次,根据当前 link 元素中的属性进行 cors 的设置;
- 然后,默认先将 credential 设为 true,如果 cors 为 Anonymous 并且存在跨域,则将 credential 置为 false;
- 最后,进行连接。
使用 Preconnect 只需要将 rel 属性设为 preconnect 即可:
<link rel="preconnect" href="//sample.com" />
当然,你也可以设置 CORS:
<link rel="preconnect" href="//sample.com" crossorigin />
需要注意的是,标准并没有硬性规定浏览器一定要(而是 SHOULD)完成整个连接过程,与 DNS Prefetch 类似,浏览器可以视情况完成部分工作。
客户端处理
Android 中采用 一些 DNS 模块(okhttp)
- 支持 H2
- 连接池复用减少延迟
- 支持 GZIP,压缩体积
- 响应缓存可以避免网络重复请求
- 配置了多个 IP 地址, 一个 IP 失败,OKhttp 自动尝试下一个
IOS 中可以采用自研 DNS 模块
- APP 启动时,缓存所有可能要用到的域名 IP,同时异步处理,客户端无需缓存
- Cache 中有域名缓存, 直接使用缓存
- 没有缓存则重新向 Http server 申请
Web 前端中的处理, 浏览器有并发数限制,做域名分散,资源分布在多个域名
- Java、php 等 API 接口放在一个域名
- 页面和样式(HTML/JS/CSS)放在一个域名
- 图片(jpg、png、gif)放在一个域名
十四、HTTP 优化
下图是请求声明周期中各个阶段的示意图,可以帮助我们理解发送请求(以及接收响应)的流程。
减少 HTTP 请求数
- css sprites
- 图片使用 DataURI、Web Font
- JS/CSS 文件合并
- JS/CSS 请求 combo
- 接口合并
- 接口存储 localstorage
- 静态资源存储 localstorage
减小 Cookie 大小
- 主站首页设置白名单
- 定期删除非白名单 Cookie
- cookie 设置子域名,防止静态资源挟带 cookie
- 设置合理的过期时长
cookie 什么时候才会自动携带呢?
如果满足下面几个条件:
1、浏览器端某个 Cookie 的 domain 字段等于 aaa.www.com 或者 www.com
2、都是 http 或者 https,或者不同的情况下 Secure 属性为 false
3、要发送请求的路径,即上面的 xxxxx 跟浏览器端 Cookie 的 path 属性必须一致,或者是浏览器端 Cookie 的 path 的子目录,比如浏览器端 Cookie 的 path 为/test,那么 xxxxxxx 必须为/test 或者/test/xxxx 等子目录才可以
Ngix 开启 Gzip 压缩
HTTP 请求头 Accept-Encoding 会将客户端能够理解的内容编码方式——通常是某种压缩算法——进行通知(给服务端)。通过内容协商的方式,服务端会选择一个客户端提议的方式,使用并在响应头 Content-Encoding 中通知客户端该选择。
Nginx 配置: nginx.conf 文件增加 gzip on
Apache 配置: AddOutputFilterByType
和 AddOutputFilter
开启 HTTPS
优点: 利于SEO
和更加安全
实施步骤:
-
购买证书(GoGetSSL / SSLs.com / SSLmate.com)
-
本地安装测试证书
// 通过HomeBrew安装
brew install mkcert
// 本地安装根证书
mkcert --insatll
// 本地生成签名
mkcert xxx.com
- 本地 nginx 配置
server{
listen 443 ssl; #启用HTTPS
server_name xxx.com #刚才的签名
ssl_certificate xxx+y.pem;
ssl_certificate_key xxx+y-key.pem;
}
使用 HTTP2
- 二进制传输
- 多路复用 TCP 连接
- header 头部压缩
- 服务端推送
缺点是如果一个TCP包丢失,会导致整个TCP的数据重传
- 升级 OpenSSL
openssl version
- 重新编译
cd nginx-xxx
./configure --with-http_ssl_module --with-http_v2_module
make && make insatll
- 配置 Nginx 的字段
server{
listen 443 ssl http2; #启用http2
server_name xxx.com #刚才的签名
ssl_certificate xxx+y.pem;
ssl_certificate_key xxx+y-key.pem;
}
- 验证 HTTP2
浏览器下查看有没有小绿锁
- 查看浏览器请求的快照 protocol 字段 是不是
h2
十五、性能优化指标
每个字段的具体含义这里不展开介绍,具体可以看 W3C 对应文档。
性能指标
- DNS 解析耗时: domainLookupEnd - domainLookupStart
- TCP 连接耗时: connectEnd - connectStart
- SSL 安全连接耗时: connectEnd - secureConnectionStart
- 网络请求耗时(TTFB): responseStart - requestStart
- 数据传输耗时: responseEnd - responseStart
- DOM 解析耗时: domInteractive - responseEnd
- 资源加载耗时: loadEventStart - domContentLoadedEventEnd
- 首包时间: responseStart - domainLookupStart
- 首次渲染时间 / 白屏时间: responseEnd - fetchStart
- 首次可交互时间: domInteractive - fetchStart
- DOM Ready 时间: domContentLoadEventEnd - fetchStart
- 页面完全加载时间: loadEventStart - fetchStart
白屏 & 首屏
白屏时间(FP)
白屏时间(First Paint):是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。
白屏时间 = 页面开始展示的时间点 - 开始请求的时间点
白屏时间是从用户开始请求页面时开始计算到开始显示内容结束,中间过程包括DNS查询、建立TCP链接、发送首个HTTP请求、返回HTML文档、HTML文档head解析完毕。
因此影响白屏时间的因素:网络、服务端性能、前端页面结构设计。
通常认为浏览器开始渲染<body>
或者解析完<head>
的时间是白屏结束的时间点。所以我们可以在html文档的head中所有的静态资源以及内嵌脚本/样式之前记录一个时间点,在head最底部记录另一个时间点,两者的差值作为白屏时间
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>白屏时间计算-常规方法</title>
<script>
window.pageStartTime = Date.now()
</script>
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/ionicons/2.0.1/css/ionicons.min.css">
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/asset/fw-icon/1.0.9/iconfont.css">
<script>
window.firstPaint = Date.now()
console.log(`白屏时间:${window.firstPaint - window.pageStartTime}`)
</script>
</head>
<body>
<div>这是常规计算白屏时间的示例页面</div>
</body>
</html>
白屏时间 = window.firstPaint - window.pageStartTime
这个方法有个缺点:无法获取解析HTML文档之前的时间信息。
首屏时间(FCP)
首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。
首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点
首屏时间要知道两个时间点:开始请求时间点和首屏内容渲染结束时间点。开始请求时间点和白屏时间一样,下面就是如何拿到首屏内容渲染结束时间点。
首屏结束时间应该是页面的第一屏绘制完,但是目前没有一个明确的API可以来直接得到这个时间点,所以我们只能智取,比如我们要知道第一屏内容底部在HTML文档的什么位置,那么这个第一屏内容底部,也称之为首屏线。
现在的问题是:首屏线在哪里?
实际情况分很多种,不同的场景有不同的计算方式。
首屏计算-标记首屏标签模块
这种方式比较简单,通过在HTML文档中,在首屏线的位置添加脚本去获取这个位置的时间。
但是在哪里添加我们只能大约估摸一个位置,以手机屏幕为例,不同型号手机屏幕大小不一样,我们取的位置可能在首屏线上面,也可以在下面,只能得到一个大约的值。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首屏时间计算-标记首屏标签模块</title>
<script>
window.pageStartTime = Date.now()
</script>
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/ionicons/2.0.1/css/ionicons.min.css">
<link rel="stylesheet" href="https://b-gold-cdn.xitu.io/asset/fw-icon/1.0.9/iconfont.css">
</head>
<body>
<div>首屏时间计算-标记首屏标签模块</div>
<div class="module-1"></div>
<div class="module-2"></div>
<script type="text/javascript">
window.firstScreen = Date.now();
console.log(`首屏时间:${window.firstScreen - window.pageStartTime}`)
</script>
<div class="module-3"></div>
<div class="module-4"></div>
</body>
</html>
首屏时间 = window.firstScreen - window.pageStartTime
::: tip 适用场景
- 首屏内不需要拉取数据,否则可能拿到首屏线获取时间的时候,首屏还是空白
- 不需要考虑图片加载,只考虑首屏主要模块
在业务中,较少使用这种算法,大多数页面需要使用接口,所以这种方法就太不常用
但是如果你的页面是静态页面,或者异步数据不影响整体的首屏体验,那么就可以使用这种办法 :::
首屏计算-统计首屏最慢图片加载
我们知道在一个页面中,图片资源往往是比较后加载完的,因此可以统计首屏加载最慢的图片是否加载完成,加载完了,记录结束时间。
如何知道首屏内哪张图片加载最慢?
我们可以拿到首屏内所有的图片,遍历它们,逐个监听图片标签的onload事件,并收集到它们的加载时间,最后比较得到加载时间的最大值。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首屏时间计算-统计首屏最慢图片加载时间</title>
<script>
window.pageStartTime = Date.now()
</script>
</head>
<body>
<img src="https://gitee.com/HanpengChen/blog-images/raw/master/blogImages/2021/spring/20210107155629.png" alt="img" onload="load()">
<img src="https://gitee.com/HanpengChen/blog-images/raw/master/blogImages/2020/autumn/article-gzh-qrcode.png" alt="img" onload="load()">
<script>
function load() {
window.firstScreen = Date.now()
}
window.onload = function () {
// 首屏时间
console.log(window.firstScreen - window.pageStartTime)
}
</script>
</body>
</html>
首屏时间 = window.firstScreen - window.pageStartTime
::: tip 适用场景
- 首屏元素数量固定的页面,比如移动端首屏不论屏幕大小都展示相同数量的内容。
但是对于首屏元素不固定的页面,这种方案并不适用,最典型的就是PC端页面,不同屏幕尺寸下展示的首屏内容不同。上述方案便不适用于此场景。 :::
首屏计算-自定义模块计算
这种算法和标记首屏的方法相似,同样忽略了首屏内图片加载的情况,这个方法主要考虑的是异步数据。
在首屏标签标记法中,是无法计算到异步数据带来的首屏空白的,所以它的适配场景十分有限
自定义模块,就是根据首屏内接口计算比较得出最迟的时间
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首屏时间计算-自定义模块计算法</title>
<script>
window.pageStartTime = Date.now()
</script>
</head>
<body>
<div class="module-1"></div>
<div class="module-2"></div>
<script type="text/javascript">
setTimeout(() => {
// 假设这里异步加载首屏要显示的文章列表数据
window.firstScreen = Date.now();
console.log(window.firstScreen - window.pageStartTime)
}, 500)
</script>
<div class="module-3"></div>
</body>
</html>
Performance
window.performance是一个浏览器中用于记录页面加载和解析过程中关键时间点的对象,放置在global环境下,通过JavaScript可以访问到它。
通过以下代码可以探测和兼容performance:
const performance = window.performance ||
window.msPerformance ||
window.webkitPerformance;
if (performance) {
// 你的代码
}
Performance-总览
- memory:显示此刻内存占用情况,是一个动态值
- usedJSHeapSize:JS对象占用的内存数
- jsHeapSizeLimit:可使用的内存
- totalJSHeapSize:内存大小限制
正常usedJSHeapSize不大于totalJSHeapSize,如果大于,说明可能出现了内存泄漏。
-
navigation:显示页面的来源信息
- redirectCount:表示如果有重定向的话,页面通过几次重定向跳转而来,默认为0
-
type:表示页面打开的方式。0-正常进入;1-通过window.reload()刷新的页面;2-通过浏览器的前进后退按钮进入的页面;255-非以上方式进入的页面。
-
onresourcetimingbufferfull:在resourcetimingbufferfull事件触发时会被调用的一个event handler。它的值是一个手动设置的回调函数,这个回调函数会在浏览器的资源时间性能缓冲区满时执行。
-
timeOrigin:一系列时间点的基准点,精确到万分之一毫秒。
- timing:一系列关键时间点,包含网络、解析等一系列的时间数据。
Performance-timing
下面我们来看下timing中的各个时间点:
-
navigationStart:同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同
-
unloadEventStart: 上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。
-
unloadEventEnd: 和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。
-
redirectStart: 第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0
-
redirectEnd: 最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0
-
fetchStart: 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。
-
domainLookupStart: DNS 域名查询开始的UNIX时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。
-
domainLookupEnd: DNS 域名查询完成的时间。如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
-
connectStart: HTTP(TCP) 域名查询结束的时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。
-
connectEnd: HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。
-
secureConnectionStart: HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。
-
requestStart: 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。
-
responseStart: 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。
-
responseEnd: 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时。(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。
-
domLoading: 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。
-
domInteractive: 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。
-
domContentLoadedEventStart: 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。
-
domContentLoadedEventEnd: 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。
-
domComplete: 当前文档解析完成,即Document.readyState 变为 ‘complete’且相对应的readystatechange 被触发时的时间戳
-
loadEventStart: load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。
-
loadEventEnd: 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0
通过上面这些时间点,我们看能计算到哪些时间:
- 重定向耗时:redirectEnd - redirectStart
- DNS查询耗时:domainLookupEnd - domainLookupStart
- TCP链接耗时:connectEnd - connectStart
- HTTP请求耗时:responseEnd - responseStart
- 解析dom树耗时:domComplete - domInteractive
- 白屏时间:responseStart - navigationStart
- DOM ready时间:domContentLoadedEventEnd - navigationStart
- onload时间:loadEventEnd - navigationStart
performance.timing记录的是用于分析页面整体性能指标。如果要获取个别资源(例如JS、图片)的性能指标,就需要使用Resource Timing API。
performance.getEntries()方法,包含了所有静态资源的数组列表;每一项是一个请求的相关参数有name,type,时间等等。
除了performance.getEntries之外,performance还包含一系列有用的方法,比如:
- performance.now()
- Performance.getEntriesByName()
- ….
上面这些方法我不具体介绍,大家可以自行查阅相关文档了解,这里我主要说一下我们可以利用getEntriesByName()这个方法来计算首屏时间:
首屏时间:performance.getEntriesByName“first-contentful-paint”].startTime - navigationStart
在评估页面是否开始渲染方面,首屏时间会比白屏时间更精确,但是二者的结束时间往往很接近。所以要根据自己的业务场景去决定到底该用哪种计算方式。
对于交互性比较少的简单网页,由于加载比较快,所以二者区别不大,因此,可以根据喜好任选一种计算方式。
对于大型的复杂页面,你会发现由于需要处理更多复杂的元素,白屏时间和首屏时间相隔比较远,这时候,计算首屏时间会更有用。
目前白屏常见的优化方案有:
- SSR
- 预渲染
- 骨架屏
优化首屏加载时间的方法:
- CDN分发(减少传输距离)
- 后端在业务层的缓存
- 静态文件缓存方案
- 前端的资源动态加载
- 减少请求的数量
- 利用好HTTP压缩
参考文档: