在遇到一个页面性能问题时,我理解的优化闭环是:分析、策略、验证和沉淀。
* 分析需要有分析数据,因此得有一个性能监控管理。
* 策略就是制订针对性的优化方案,解决当前遇到的问题。
* 验证的对象上述策略,判断方案是否有效,同样需要数据支撑。
* 沉淀就是将解决过程文档化、通用化,能够总结成一套实际方案、优化规则等。
  这其中非常关键的一步是需要采集到性能数据,并且得有个可视化后台查看数据变化。
  在之前已经自制了一个[性能优化平台](https://www.cnblogs.com/strick/p/14578711.html),采集前端性能参数的 SDK 叫[shin.js](https://github.com/pwstrick/shin-admin/blob/main/public/shin.js#L226)。
## 一、优化的三部分
  在文章开头,我想先聊聊网页优化的三部分:网络,渲染和容器。
  第一部分的网络就是提升传输速度,可优化的手段包括 gzipped压缩、CDN、HTTP 缓存、HTTP 2.0协议、并发请求等。
  像 HTTP 缓存分为强缓存和协商缓存,请求首部和浏览器配合完成资源的缓存机制,下图摘自《[前端程序员面试笔试宝典](https://book.douban.com/subject/30324146/)》。
:-: ![](https://img.kancloud.cn/39/89/3989d605b85acedbafec9741e2eb64e1_960x242.png =600x)
  第二部分的渲染就是 CRP 优化(关键渲染路径),CRP 是指浏览器从接收资源到渲染像素的过程。
  优化的点包括资源数、字节数和加载时序。现代化的 webpack 构建工具就会对资源做前两项的优化处理,包括压缩文件、合并文件、优化包的引入等。
  加载时序就包括日常都会用的图片懒加载和预加载、脚本的延迟(defer)、异步(async)和预加载([preload](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Link_types/preload))等。
  对于比较庞大的首页,可以先将那些能阻塞网页首次渲染的关键资源载入,其余资源都延迟载入,以此提升页面打开速度。
  第三部分的容器(WebView)就是借用端的能力,让 APP 配合优化网页。
  例如[预请求](https://www.cnblogs.com/strick/p/14918217.html),将请求接口的时机前置到容器打开之时,下面是一张实现流程图。
:-: ![](https://img.kancloud.cn/9b/f8/9bf87beac1a3f77c86445fe860c811ac_705x750.png =600x)
  还有一种静态资源缓存至客户端本地,当时与公司客户端讨论此方案时,他们觉得每次拦截请求会损伤性能,后面就采用了折中的办法。
  就是他们去主动请求特定地址的静态资源,然后开放接口让我可以去读取本地资源,也就是说由 Web 来控制是否读取缓存资源。
## 二、问题引出
  现在言归正传,回到本次的优化中来。
  为了提升页面产出率,联合 UI 设计构建了一套可配置的[通用活动模板](https://www.cnblogs.com/strick/p/15928830.html)。
  活动上线后,就查看了性能数据,情况很不理想,如下图所示。
![](https://img.kancloud.cn/ac/38/ac3812ce8a686e3a8680190f71292f03_2518x842.png =800x)
  FP(白屏)时间大部分都在 2 秒以上,取平均值更是在 3 秒左右。[Google的报告](https://support.google.com/webmasters/answer/9205520?hl=zh-Hans)指出:
* 如果网页加载时间从 1 秒增加到 3 秒,跳出率就会提高**32%**。
* 如果网页加载时间从 1 秒增加到 6 秒,跳出率就会上升**106%**。
## 三、数据排查
  在数据库中,将指定的性能数据记录导出到 Excel 中。
  翻了一条后发现,性能问题集中在 DOM 中。
~~~
{
"unloadEventTime": 0,
"loadEventTime": 1,
"interactiveTime": 1255,
"parseDomTime": 1075,
"initDomTreeTime": 721,
"readyStart": 5,
"redirectCount": 0,
"compression": 0,
"redirectTime": 0,
"appcacheTime": 0,
"lookupDomainTime": 0,
"connectSslTime": 0,
"connectTime": 0,
"requestTime": 119,
"requestDocumentTime": 119,
"responseDocumentTime": 0,
"TTFB": 534,
}
~~~
  JSON 中的 interactiveTime、parseDomTime 和 initDomTreeTime 消耗的时间都不短,计算规则如下所示。
~~~
/**
* 解析 DOM 树结构的时间
* 期间要加载内嵌资源
* 反省下你的 DOM 树嵌套是不是太多了
*/
api.parseDomTime = timing.domComplete - timing.domInteractive;
/**
* 请求完毕至DOM加载耗时
*/
api.initDomTreeTime = timing.domInteractive - timing.responseEnd;
/**
* 首次可交互时间
*/
api.interactiveTime = timing.domInteractive - timing.fetchStart;
~~~
  参考 W3C 第二版性能参数图可知,慢的地方集中在 Processing 阶段。
:-: ![](https://img.kancloud.cn/e4/44/e4444c72fa8a5b8d5f523628b3280eef_1280x406.png =800x)
## 四、Chrome DevTools
  打开 Chrome DevTools 中的 Performance 一栏,录制后,可在火焰图中看到长任务。
  点击 Long task 链接,会跳转到[使用 RAIL 模型衡量性能](https://web.dev/rail/)一文。
![](https://img.kancloud.cn/87/94/87940d7c9125935708e9c69bf9cf2d6a_1930x1190.png =800x)
  在 PC 浏览器中打开肯定会比在手机中快,但即使如此,还是出现了性能瓶颈,说明这里是真的慢。
  蓝底的 DCL 是 [DOMContentLoaded](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/DOMContentLoaded_event)事件,在 HTML 文档被完全加载和解析后触发,绿底的 FP 就是白屏时间。
  黄底的 Evaluate Script 表示加载 JavaScript 脚本,Compile Script 表示执行 JavaScript 脚本。
  再来看看网络请求瀑布图,下图中的蓝线就是 DCL,可以清晰的看到,蓝线之前在加载的基本都是 JavaScript 脚本。
:-: ![](https://img.kancloud.cn/9b/c7/9bc741d0abebcbac08224f21888c1587_1928x1058.png =800x)
  由此可知,加载的脚本有点多,并且有一个 chunk-vendors.js 脚本还比较大,下载时间有点长(依据蓝色块)。
## 五、代码分析
  定位到了问题根源,那就直接查看基于 Vue 的代码是怎么写的了。
**1)HTML**
  下面是编译后的页面 HTML 结构,只列出了关键部分。
~~~html
<!DOCTYPE html>
<html lang=en>
<head>
<script src=https://res.wx.qq.com/open/js/jweixin-1.6.0.js></script>
<script src=//www.xxxx.com/flexible/flexible.js></script>
<script src=//www.xxxx.co/files/js/baidu.js></script>
<script src=//www.xxxx.co/files/js/shin.js></script>
<link href=//www.xxxx.me/game/css/operation37.cba04f10.css rel=preload as=style>
<link href=//www.xxxx.me/game/js/chunk-lodash.152ef24b.js rel=preload as=script>
<link href=//www.xxxx.me/game/js/chunk-lottie.23b9982e.js rel=preload as=script>
<link href=//www.xxxx.me/game/js/operation37.fa5f5378.js rel=preload as=script>
<link href=//www.xxxx.me/game/css/chunk-vendors.779f7d1d.css rel=stylesheet>
<link href=//www.xxxx.me/game/css/operation37.cba04f10.css rel=stylesheet>
</head>
<body>
<div id=app></div>
<script src=//www.xxxx.me/game/js/chunk-vendors.ca022e99.js></script>
<script src=//www.xxxx.me/game/js/operation37.fa5f5378.js></script>
</body>
</html>
~~~
  首先在 head 中,引入了大量的 JavaScript 脚本,flexible.js 其实在构建时可以内联,不需要网络访问。
  然后 jweixin-1.6.0.js 和 baidu.js 这两个脚本完全可以延迟加载,后者就是增加百度统计的脚本。
  接着就是 shin.js 需要做压缩处理,可以减少 50% 以上的尺寸。
  在 link 元素中,使用了[preload](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Link_types/preload),表示可并行的预加载,并且不会执行,这是提升页面性能的一种手段。
  虽然第三方的库(chunk-vendors.ca022e99.js)和业务主逻辑(operation37.fa5f5378.js)两个脚本声明在 body 中。
  但是主结构就是个空的 div,因此在加载和运行时就会延长 DOM 的解析,影响白屏时间。
**2)vendors 优化**
  Vue 内置了一条命令,可以查看每个脚本的尺寸以及内部依赖包的尺寸。
  在下图中,vendors.js 的原始尺寸是 3.76M,gzipped 压缩后的尺寸是 442.02KB,比较大的包是 lottie、swiper、moment、lodash 等。
:-: ![](https://img.kancloud.cn/ee/0c/ee0cdbdec497af0f64215036f050adcc_3224x1835.png =800x)
  这类比较大的包可以再单独剥离,不用全部聚合在 vendors.js 中。
  在 vue.config.js 中,配置 config.optimization.splitChunks(),如下所示,参数含义可[参考官网](https://webpack.docschina.org/plugins/split-chunks-plugin)。
~~~
config.optimization.splitChunks(
{
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
lottie: {
name: 'chunk-lottie',
test: /[\\/]node_modules[\\/]lottie-web[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
},
swiper: {
name: 'chunk-swiper',
test: /[\\/]node_modules[\\/]_swiper@3.4.2@swiper[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
},
lodash: {
name: 'chunk-lodash',
test: /[\\/]node_modules[\\/]lodash[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
}
}
}
)
~~~
  在经过一顿初步操作后,原始尺寸降到 2.4M,gzipped 压缩后的尺寸是 308.64KB,比之前少了 100 多 KB。
:-: ![](https://img.kancloud.cn/60/e2/60e2b9c4afbb83c585273aee6f9afd63_3224x1836.png =800x)
  现在在入口处需要单独声明依赖的包,否则不会自动引入。
~~~
pages: {
operation37: {
entry: 'src/pages/operation37/index.js',
template: 'src/pages/operation37/index.html',
filename: 'operation37.html',
title: '榜单配置页面',
chunks: ['chunk-lottie', 'operation37', 'chunk-vendors']
},
}
~~~
  其实大部分的 H5 页面都比较简单,可能就使用了包的一个小功能,那完全可以自己用代码实现,这样就不必依赖那个大包了。
  后面就是在代码逻辑层面的优化,核心就是减少脚本尺寸。优化后,再去观察数据的变化。
**3)CDN加速**
  之前部分静态资源采取了 CDN 加速,现在需要将 game 下面中的静态资源全部走 CDN。
  在云端配置些参数,就能走 CDN。不过,第一次没有配置好,没有配置转发路径,造成了严重的线上问题。
  第二次就比较谨慎,在测试环境将之前碰到的问题都验证后,才最终在线上配置。
  白屏时间占比变化:
* 1 秒内的占比从 77.3% 最高提升至 78.7%
* 1 - 2 秒占比从 15.6% 最高提升至 18.7%
* 2 - 3 秒占比从 4% 最低下降至 1.8%
* 3 - 4 秒占比从 1.1% 最低下降至 0.6%
* 4 秒以上的占比从 2.1% 最低下降至 1.4%
参考资料:
[长的 JavaScript 任务是否会延迟您的交互时间?](https://web.dev/i18n/zh/long-tasks-devtools/)
[狙杀页面卡顿 —— Performance 工具指北](https://zhuanlan.zhihu.com/p/41017888)
[chrome performance看浏览器渲染过程](https://blog.csdn.net/It_rod/article/details/79661739)
[深入理解浏览器解析渲染 HTML](https://juejin.cn/post/6844904131346300942)
[Vue CLI 项目页面打开时间优化:从16秒到2秒内](http://www.zuo11.com/blog/2020/11/vue_cli_slow.html)
[preload 让加载和解析解耦](https://juejin.cn/post/6844903854690009102)
*****
> 原文出处:
[博客园-Web优化躬行记](https://www.cnblogs.com/strick/category/1795726.html)
[知乎专栏-Web优化躬行记](https://zhuanlan.zhihu.com/c_1260996761008627712)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020