前端性能优化是前端面试过程中永远逃不开的问题,尤其是针对代码量巨大的单页应用,优化更为重要。这里结合项目经历,总结一下项目中做过的性能优化。
首先,我们需要思考一下,前端出现性能问题可能有哪些层面的原因?加载速度慢?页面运行卡,动画卡顿不流畅?
所以,从用户角度来看,前端性能优化可以分为两个方面。一个是页面加载的很快,另一个是页面使用起来很流畅。因此,对性能优化的探索,我们可以分为页面加载时间跟页面运行效率两个方向来进行研究。
1. 页面加载速度
移动端一个h5页面,从用户点 H5 链接到页面渲染完成展示给用户,需要经历的过程,如下图:
其中 Webview 容器初始化、静态资源加载以及对后端接口的请求,耗时是比较多的,并且这个耗时页面一定处于白屏阶段,项目中通常会对这三块给出一些常规的优化方案,渲染的耗时优化本文不论述。
1.1 webview容器初始化
我们采用过业界常用的全局WebView的方式,在我们的rn首页,我们会在用户进入首页时候,就初始化一个后续资源页面的的WebView待用,并隐藏,当点击查询进入资源页面的时候,直接使用这个WebView加载对应网页,并展示。
1.2 静态资源加载优化
这块涉及到的内容很多,主要是通过webpack打包机制,浏览器缓存机制,cdn加速,域名扩展等方式优化静态资源。
1.2.1 webpack打包机制
- 这块可以配合浏览器的缓存机制一起考虑。浏览器缓存结合通过webpack打包给静态资源路径中加入hash的方式共同控制静态资源(背景:为了利用浏览器和 CDN 的缓存,通常会给静态资源设置一个比较长的缓存时间,在有内容更新的时候,通过在静态资源路径中加入 hash 的方式来使缓存失效,更新文件。)
具体使用可以参考下面其他部分 - 使用splitChunks对公共代码进行分包,避免公共代码被重复打包加载。
- 按需加载,相应页面加载其对应代码使用按需加载(懒加载)的方式可以有效提高页面性能(import().then()、require.ensure)
1.2.2 骨架屏
骨架屏也是在移动端页面首屏优化的一个重要手段,在页面数据未准备好的情况,相比与枯燥的白屏页面而言,展示骨架屏能给用户一个好的感官体验。但是如何生成质量高的骨架屏也是一个难点,需要综合考虑 ROI 来选择是否使用骨架屏。
1.2.3 完善的cdn和缓存方案
- 对变化比较少的资源配置超长时间的本地缓存 — 节省带宽
- 对资源采用内容摘要作为缓存更新依据 — 精确的缓存控制静态资源
- 静态资源实现CDN部署 —优化网络请求
- 更资源发布路径实现非覆盖式发布 — 平滑升级
1.2.4 SSR服务端渲染
1.2.5 其他
- 使用多个域名,将同一页面的资源分散到不同域名下,提升连接上限。 Chrome有个机制,同一个域名同时最多只能建立6个TCP连接
- 资源请求合并,过多分散的资源包会产生过多的网络请求,但也不能随意合并,最佳的方式是按照页面或者模块进行划分,并配置 async 属性来异步加载 script 脚本。
- 资源压缩
1.3. 数据请求的优化
- 请求合并,对多个接口请求合并处理,可以采用在后端提供接口的基础上,在进行node端的一层包装,以获得更符合前端Ui的数据模型。
1.4 渲染优化
2.1 css和js的装载
- css 样式表置顶,防止页面在没有css样式的情况下渲染出来
- js 脚本置底,不阻塞页面的渲染和css的加载,使css资源尽量在第一批并发下载中,让页面更快的呈现给用户。
- 合理使用js的异步加载(defer/aysnc)
2.2 尽量避免避免重排和重绘
- 用translate(重绘)替代top(回流)改变
- 批量修改dom
- 添加 CSS3 样式启用 GPU 硬件加速
- 优化dom操作
2. 页面运行流畅
为了保证页面运行效率,快速响应用户的交互,主要需要进行的是代码层面的优化。
- 简单动画尽量只使用transform、opacity、transition等属性完成
- 使用requestAnimationFrame代替setTimeout、setInterval控制动画频率
- 对将要使用动画的部分,开启GPU硬件加速
- 对高频触发事件使用防抖或者节流
- 框架层面的优化,比如在使用react的时候,通过合理的使用shouldcomponetupdate或者hooks来减少不必要的render。
- 事件委托,利用事件冒泡的机制来处理子元素事件的绑定,将子元素的 DOM 事件,交由它们的父元素来进行处理,可以有效降低页面的开销等
- 懒加载
- 虚拟列表,像原生APP一样,只渲染可视区域,但是给用户无感知的滚动体验。
其他部分
1. webpack输出文件的hash, chunkhash, contenthash?
通过一个例子说明:
module.exports = {
entry: {
main:"main.js" // 业务代码
},
output: {
path: "/dist",
// filename: "[name].[hash].js"
filename: "[name].[chunkhash].js"
},
optimization: {
moduleIds: 'hashed',
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: "vendors", // 公共模块
chunks: "all"
}
}
}
}
}
output.filename, 如果采用hash的话,main和公共模块vendor的hash值会相同,这就有一个问题,每次业务代码改变也会导致公共模块改变。
如果采用chunkhash,它会使用它自己的模块中的内容来生成哈希值, vendor和main就会有不同的hash值,业务代码改变也不会导致公共模块改变。
但是还有一个问题,随着业务依赖的增加,比如main中增加了依赖,我们打包的时候希望只有main文件的hash改变,而事实是main和vendor都变化了,这不是我们所希望的,这个和webpack的chuckid有关,当你增加了依赖,vendor默认的chunkid变化了就会重新生成hash,这时候可以使用webpack 4提供的moduleIds: ‘hashed’,来固定公共包。
除了chunkhash,还可以使用更精确的contenthash,举个例子,如果在源码中,index.css被index.js引用了,所以共用相同的chunkhash值。但是这样子有个问题,如果index.js更改了代码,css文件就算内容没有任何改变,由于是该模块发生了改变,导致css文件会重复构建,这时候可以用contenthash。
2 ManifestPlugin插件的使用
在SSR开发时,前端打包后,node后端就可以通过这个json数据,返回正确资源路径的html模板
3. RAF:requestAnimationFrame
动画是由浏览器按照一定的频率一帧一帧的绘制的,由css实现的动画的优势就是浏览器知道动画的开始及每一帧的循环间隔,能够在恰当的时间刷新UI,给用户一种流畅的体验,而js的setInterval或setTimeout实现的JavaScript动画就没有这么可靠了,因为浏览器压根就无法保证每一帧渲染的时间间隔,一般情况下,每秒平均刷新次数能够达到60帧,就能够给人流畅的体验,即每过 1000/60 毫秒渲染新一帧即可,但从上面的例子知,这一点单靠定时器是无法保证的。
为此,requestAnimationFrame应运而生,其作用就是让浏览器流畅的执行动画效果。可以将其理解为专门用来实现动画效果的api,通过这个api,可以告诉浏览器某个JavaScript代码要执行动画,浏览器收到通知后,则会运行这些代码的时候进行优化,实现流畅的效果,而不再需要开发人员烦心刷新频率的问题了。
3.1 requestAnimationFrame的优势体现在
- CPU节能:使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,这完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。
- 函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。
3.2 requestAnimationFrame的使用
requestAnimationFrame的用法与setTimeout很相似,只是不需要设置时间间隔,回调函数会在浏览器重绘之前调用。它返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数。