# 性能优化总结

# 什么是 Web 性能优化
前端性能优化的目的: 让用户访问网站开始到页面完整展示出来的过程中, 通过各种优化策略和优化方法, 让页面加载的更快, 让用户操作响应更加及时, 给用户带来更好的用户体验
# 为什么需要性能优化
研究表明, 网页性能差直接加速产品的衰败, 也影响网站收入(广告), 因此我们需要提升 Web 性能从而提升用户体验, 公司营收等
# 优化步骤
以下笔者来详述所有常见的优化手段
# 一、图片优化
# 图片分类
图片格式众多, 附上图片分类脑图 (opens new window)

# 图片格式选取
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 服务器建立链接。
这里我们需要了解的是
- 首先,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 (opens new window) 时,浏览器大致做了如下处理:
- 首先,解析 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 什么时候才会自动携带呢?
NAME=VALUE | 赋予Cookie的名称和其值(必须项) |
---|---|
expires=DATE | Cooke的有效期(若不明确指定则默认为浏览器关闭前为止) |
path=PATH | 将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录) |
domain=域名 | 作为Cookie适用对象的域名(若不指定则默认为创建Cookie的服务器的域名) |
Secure | 仅在HTTPS安全通信时才会发送Cookie |
HttpOnly | 加以限制,使Cookie不能被JavaScript脚本访问 |
如果满足下面几个条件:
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
适用场景
- 首屏内不需要拉取数据,否则可能拿到首屏线获取时间的时候,首屏还是空白
- 不需要考虑图片加载,只考虑首屏主要模块
在业务中,较少使用这种算法,大多数页面需要使用接口,所以这种方法就太不常用
但是如果你的页面是静态页面,或者异步数据不影响整体的首屏体验,那么就可以使用这种办法
# 首屏计算-统计首屏最慢图片加载
我们知道在一个页面中,图片资源往往是比较后加载完的,因此可以统计首屏加载最慢的图片是否加载完成,加载完了,记录结束时间。
如何知道首屏内哪张图片加载最慢?
我们可以拿到首屏内所有的图片,遍历它们,逐个监听图片标签的 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
适用场景
- 首屏元素数量固定的页面,比如移动端首屏不论屏幕大小都展示相同数量的内容。
但是对于首屏元素不固定的页面,这种方案并不适用,最典型的就是 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"](0)\].startTime - navigationStart
在评估页面是否开始渲染方面,首屏时间会比白屏时间更精确,但是二者的结束时间往往很接近。所以要根据自己的业务场景去决定到底该用哪种计算方式。
对于交互性比较少的简单网页,由于加载比较快,所以二者区别不大,因此,可以根据喜好任选一种计算方式。
对于大型的复杂页面,你会发现由于需要处理更多复杂的元素,白屏时间和首屏时间相隔比较远,这时候,计算首屏时间会更有用。
目前白屏常见的优化方案有:
- SSR
- 预渲染
- 骨架屏
优化首屏加载时间的方法:
- CDN 分发(减少传输距离)
- 后端在业务层的缓存
- 静态文件缓存方案
- 前端的资源动态加载
- 减少请求的数量
- 利用好 HTTP 压缩
参考文档: