前端性能优化

思维导图获取
基础的 Web 技术层面的优化
Web服务器优化
- 开启压缩
content-encoding 有五个参数值: gzip、compress、deflate、identity、br(Brotli)。
它表示消息主体进行了何种方式的内容编码转换。这个消息首部用来告知客户端应该怎样解码才能获取在 Content-Type 中标示的媒体类型内容。
Gzip: 基于 DEFLATE 算法,它是 LZ77 和霍夫曼编码的组合,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 Gzip 编码是一种用来进 Web 应用程序性能的技术,Web 服务器和客户端(浏览器)必须共同支持 Gzip,当下主流的浏览器都是支持 Gzip 压缩,包括 IE6、IE7、IE8、IE9、FireFox、Google Chrome、Opera 等。
br: br是谷歌压缩团队2015年发布的,特别侧重于HTTP压缩,Brotli 通过变种的 LZ77 算法、Huffman 编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比,它有着更高的压缩效率
针对常见的 Web 资源内容,Brotli 的性能相比 Gzip 提高了 17-25%;
当 Brotli 压缩级别为 1 时,压缩率比 Gzip 压缩等级为 9(最高)时还要高;
在处理不同 HTML 文档时,Brotli 依然能够提供非常高的压缩率。
比其他算法提供更快的解压与压缩算法
compress: 已废弃
identity: 没有压缩,默认值
deflate:采用 deflate 算法,跟 Gzip 差不多
关于LZ77和哈夫曼编码的原理看这里gzip压缩的LZ77算法和哈夫曼编码实现原理 #36 - 使用HTTP 2.0
HTTP 1.x 与 HTTP 2.0 的区别
页面加载阶段
- dns预解析(dns-prefetch)
DNS Prefetch 能效缩短了DNS的解析时间,如果浏览器最近将一个域名解析为IP地址,所属的操作系统将会缓存,下一次DNS解析时间可以低至0-1ms。 如果结果不在系统本地缓存,则需要读取路由器的缓存,则解析时间的最小值大约为15ms。如果路由器缓存也不存在,则需要读取ISP(运营商)DNS缓存,一般像taobao.com、baidu.com这些常见的域名,读取ISP(运营商)DNS缓存需要的时间在80-120ms,如果是不常见的域名,平均需要200-300ms。一般的网站在运营商这里都能查询的到,所以普遍来说DNS Prefetch可以给一个域名的DNS解析过程带来15-300ms的提升,尤其是一些大量引用很多其他域名资源的网站,提升效果就更加明显了。详细用法点这里 - 使用CDN
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。
CDN加速原理CDN通过将资源部署到世界各地,使得用户可以就近访问资源,加快访问速度。要接入CDN,需要把网页的静态资源上传到CDN服务上,在访问这些资源时,使用CDN服务提供的URL。
由于CDN会为资源开启长时间的缓存,例如用户从CDN上获取了index.html,即使之后替换了CDN上的index.html,用户那边仍会在使用之前的版本直到缓存时间过期。业界做法:
HTML文件:放在自己的服务器上且关闭缓存,不接入CDN
静态的JS、CSS、图片等资源:开启CDN和缓存,同时文件名带上由内容计算出的Hash值,这样只要内容变化hash就会变化,文件名就会变化,就会被重新下载而不论缓存时间多长。
另外,HTTP1.x版本的协议下,浏览器会对于向同一域名并行发起的请求数限制在4~8个。HTTP2.x取消了请求数量的限制,具体看这里。 那么把所有静态资源放在同一域名下的CDN服务上就会遇到这种限制,所以可以把他们分散放在不同的CDN服务上,例如JS文件放在js.cdn.com下,将CSS文件放在css.cdn.com下等。这样又会带来一个新的问题:增加了域名解析时间,这个可以通过dns-prefetch来解决 来缩减域名解析的时间。形如**//xx.com 这样的URL省略了协议**,这样做的好处是,浏览器在访问资源时会自动根据当前URL采用的模式来决定使用HTTP还是HTTPS协议。
- 静态资源的压缩与合并
html压缩:压缩不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如HTML注释也可以被压缩
css压缩:将无效的样式代码删除,将相同语义的css代码进行合并
Js压缩与混淆:将无效字符、注释删除,变量名、函数名变为a、b、c...
合并的优点:
- 减少网络请求(HTTP 1.x 连接数量受限制)
- 每一次网络请求都可能会存在丢包的情况,减少丢包的影响
- 减少代理服务器可能会被断开的影响
合并的缺点:
- 首屏渲染问题,文件合并必然会增大文件的体积,如果页面的显示依赖于某个文件,但是这个文件因为体积太大迟迟加载不加来,必须会增加页面的白屏时间。这个问题在使用vue或者react之类的前端框架的时候尤为严重。
- 缓存失效问题,如果一个合并后的js文件abc.js是由a.js和b.js和c.js合并形成的,那这三个文件的其中一个发生改变都必然会导致abc.js文件缓存的失效。
关于压缩与合并的更多内容,点这里
- 减少https请求
同一域名下,浏览器对并发请求的数目是有限制,Chrome浏览器最多允许对同一个域名Host建立6个TCP连接,不同的浏览器有所区别。
HTTP/1.1中,单个TCP连接,在同一时间只能处理一个http请求,虽然存在Pipelining技术支持多个请求同时发送,但由于实践中存在很多问题无法解决,所以浏览器默认是关闭,所以可以认为是不支持同时多个请求。
HTTP2提供了多路传输功能,多个http请求,可以同时在同一个TCP连接中进行传输。
- 异步加载defer,async
可以使js文件跟文档元素异步并行加载
相同点:使js可以异步加载
不同点:加了async的js,在文档加载和渲染的过程中就可以执行。加了 defer 的js,需要等到文档元素解析完成后,DOMContentLoaded事件触发执行之前。

- 服务端渲染ssr
服务端渲染本质上是本该浏览器做的事情,分担给服务器去做
优点:1. 提升首屏加载 2. 利于SEO
缺点:加重服务器负担 - 使用浏览器缓存
浏览器将网络资源存储在本地,达到减少带宽,降低网络负荷,加快页面加载速度的目的。
浏览器的缓存机制
页面渲染阶段
- css放前面,js放后面
为什么css要放头部,js放尾部以及async、defer该如何处理? - 减少dom查询,多次使用的保存为变量
- 减少dom操作,统一通过dom片段操作
使用 DocumentFragment 暂存 DOM,整理好以后再插入 DOM 树
使用 className 来操作元素的样式
减少用 JavaScript 修改布局 - 事件函数的节流和防抖
防抖与节流 - 图片懒加载
方法一:
用 insectionObserver 实现,代码示例
方法二:
用监听scroll 事件来判断图片是否进入视口,为了提高性能,会用到防抖,但是效果不如 insectionObserver - 尽早进行操作, domcontentload 与 load
domcontentload: 在 html 文档加载完毕,并且 html 所引用的内联 js、以及外链 js 的同步代码都执行完毕后触发。
load: 当页面 DOM 结构中的 js、css、图片,以及 js 异步加载的 js、css 、图片都加载完成之后,才会触发 load 事件。
domcontentload 中操作可以减少 dom reflow repaint 的次数。
关于 domcontentload 与 load 的信息,看这里
webpack优化
- 优化打包文件大小
- 去除无用代码,使用es6 modules(import 和 export),webpack启用mode:"production" 来使用 Tree Shaking
- 压缩代码,使用 UglifyJS 压缩 js,cssnano 压缩 css
- 提取公共代码(CommonsChunkPlugin)
- 按需引入,或有些资源可以通过cdn引入
webpack 内置,通过 chunkFilename 来设置 - 压缩图片体积,也可使用图片使用webp,优雅降级处理
- 优化打包速度
- 利用多线程提升打包速度,HappyPack
- 预先编译资源模块(DllPlugin)
一般来说第三方模块是不会变化的,所以我们想只要在第一次打包的时候去打包一下第三方模块,并将第三方模块打包到一个特定的文件中,当第二次 webpack 进行打包的时候,就不需要去 node_modules 中去引入第三方模块,而是直接使用我们第一次打包的第三方模块的文件就行。
webpack.DllPlugin 就是来解决这个问题的插件,使用它可以在第一次编译打包后就生成一份不变的代码供其他模块引用,这样下一次构建的时候就可以节省开发时编译打包的时间。 - 缩小loader的文件搜索范围(比如babel-loader)
- 使用缓存,比如
loader: 'babel-loader?cacheDirectory=true'
3.webpack 版本升级
- 一般版本升级都会带来速度的提升,并且效果很明显
vue代码层面优化
- 不需要做响应式的数据,不要放在data中或者使用Object.freeze,但如果是嵌套对象,则需要递归冻结
响应式数据:每个Vue实例都会代理其data对象里所有的属性,只有这些被代理的数据是响应式的,在其数据改变时视图也会随之更新。
在每个vue组件中都有一个data对象。不要把所有数据都放在data中,只把需要做响应式的数据放在data对象中;原因是:如果一个数据存在于data中,数据会被劫持,vue会给数据添加一个getter(获取数据),一个setter(设置数据),性能不会高。
- 在模板里不要写太多表达式
- v-if 和 v-for 不要在一起使用,v-for 使用 key
v-for比v-if优先级高,所以使用的话,每次v-for都会执行v-if,造成不必要的计算,影响性能,尤其是当之需要渲染很小一部分的时候。例如:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id"> {{ user.name }} </li></ul>
<ul>
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
</ul>
优化方法:
使用computed 计算出
<div>
<div v-for="(user,index) in activeUsers" :key="user.index" >
{{ user.name }}
</div></div>
data () { // 业务逻辑里面定义的数据
return {
users: [{
name: '111111',
isShow: true
}, {
name: '22222',
isShow: false
}]
}
}
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isShow;//返回isShow=true的项,添加到activeUsers数组
})
}}
- v-show 和 v-if 区分使用
v-if 会销毁组件、重新创建组件,v-show只是控制组件的显示隐藏,所以 v-if 性能消耗更高 - computed 和 watch 区分使用
computed 是一个计算属性,只有依赖的响应式 property 变化才会重新计算,否则取缓存中的值。
watch 是监听某个值的变化,然后去做一些处理,作用更多的是「观察」 - 纯展示的长列表不使用数据双向绑定
- 使用虚拟列表技术处理非常长或者无限滚动的列表
参考:
http://caibaojian.com/vue-optimization.html
https://www.cnblogs.com/moqiutao/p/11079722.html
https://zhuanlan.zhihu.com/p/147882950
https://www.jianshu.com/p/86d4cc0a55c0
https://www.cnblogs.com/soyxiaobi/p/9519895.html