众所周知,Vue帮我们做了很多"脏活累活",例如:数据的双向绑定;Virtual Dom技术等,我们可以把大部分的时间抽出来去实现我们的业务逻辑。但是,我们仍然需要关注vue的首屏优化,webpack的配置优化,资源loading的快慢,vue项目运行时的性能优化等等。为了更好地提升我们的网站用户体验,下面我将通过实际项目的优化实践分模块总结:
基础web技术层面的优化
webpack打包配置方面的优化
vue代码层面的优化
其他优化
为了更了解整个性能优化过程,我们先来梳理一件事: 从输入URL 到页面加载完成发生了什么事?
image (6).png
用户输入URL
浏览器先检查本地是否有对应的IP地址,若找到则返回对应的IP地址。若没找到则请求上级DNS服务器,直至找到或到根节点
TCP连接进行三次握手
浏览器发送HTTP请求资源/数据
服务端处理请求进行回应
浏览器接收HTTP响应
浏览器渲染页面,构建DOM树
浏览器关闭TCP连接(四次挥手)
从以上过程可以看出整个处理响应过程其实是三部分: 客户端请求,服务端响应,客户端接收响应,如此可以发现前端能做的优化其实是第一和第三部分:让客户端做出更有效且高效的请求,让客户端接收响应后更快速的渲染页面,实现功能。下面我们具体分析如何进行优化:
以下是浏览器渲染页面的过程:
企业微信截图_039a89cd-22f9-444b-b751-243aaf102bf0.png
DOMTree: 解析html构建DOM树。
CSSOMTree : 解析CSS生成CSSOM规则树。
RenderObjectTree: 将DOM树与CSSOM规则树合并在一起生成渲染对象树。
Layout: 遍历渲染树开始布局(layout),计算每个节点的位置大小信息。
Painting: 将渲染树每个节点绘制到屏幕。
具体的一些实践做法:
1.防止阻塞渲染
由于CSS和JS会影响DOM树和CSSOM的构建,所以浏览器在加载CSS和JS文件时会阻塞HTML的解析,为了避免阻塞,我们可以做以下优化:
css 放在head标签内,提前加载。这样做的原因是: 通常情况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成之前,页面不会被渲染,放在顶部让样式表能够尽早开始加载。但如果把引入样式表的 link 放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,很可能会造成页面闪烁。
js文件放在body底部,防止阻塞解析
首页不使用或者不改变dom和css的js文件使用 defer 和 async 属性进行异步加载,不阻塞解析
2.减少重绘和回流
尽量少用js访问dom节点和css属性,能用css解决的问题就不要用js去做
可能会涉及动画的HTML元素可以使用使用fixed或absolute的定位,修改对应的CSS样式就不会产生回流了
img标签设置高宽,以减少重绘重排
尽量用 transform 来做形变和位移,减少使用left,top,这样不会造成回流
3.减少DOM和CSSOM的构建时间
DOM的层级尽量不要太深,否则会增加DOM树构建的时间,js访问深层的DOM也会造成更大的负担。
减少 CSS 嵌套层级和选择适当的选择器
需要服务端配合的操作:
gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右。重启服务之后可以看到:
微信截图_20210406123135.png
vue2.0使用webpack打包,会帮我们安装好compression-webpack-plugin插件,并生成好对应的代码:
if (config.build.productionGzip) { const CompressionWebpackPlugin = require('compression-webpack-plugin') webpackConfig.plugins.push( new CompressionWebpackPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp( '\\.(' + config.build.productionGzipExtensions.join('|') + ')$' ), threshold: 10240, minRatio: 0.8 }) )}
在index.js中开启GZIP开关,剩下的就是要服务器去支持GZIP了
image.png
缓存的目的是简化资源的请求路径,比如某些静态资源在客户端已经缓存了,再次请求这个资源,只需要使用本地的缓存,而无需走网络请求去服务端获取。具体的缓存规则服务器会将其放入http响应报文的response headers中与请求结果一起返回给浏览器。
缓存类型:
1587717684639-78b0468f-138f-4470-981a-b56bcf6c0cae.png
缓存过程:
微信截图_20210406123526.png
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率
chrome开发者工具性能分析工具 Performance 可以帮助我们监控并分析页面的性能情况,进而去采取对应的优化措施:
打开 Chrome 开发者工具,切换到 Performance 面板
点击 Record 开始录制
刷新页面或展开某个节点
点击 Stop 停止录制
image (1).png
安装
npm i webpack-bundle-analyzer -D
使用
//在webpack.prod.conf.js文件中加入以下代码const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPluginnew BundleAnalyzerPlugin({ analyzerMode: 'server', //server | static | disabled analyzerHost: '127.0.0.1', // 默认值:127.0.0.1。 将在服务器模式下用于启动HTTP服务器的主机。 analyzerPort: 8889, // 默认值:8888。将在服务器模式下用于启动HTTP服务器的端口。 reportFilename: 'report.html', // 默认值:report.html。 捆绑将在静态模式下生成的报告文件的路径。 相对于bundle输出目录(在webpack配置中是output.path)。 defaultSizes: 'parsed', // 默认值:已解析。 默认情况下在报告中显示的模块大小。 大小定义部分描述了这些值的含义。 openAnalyzer: true, // 默认值:true。 在默认浏览器中自动打开报告。 generateStatsFile: false, // 默认值:false。 如果为true,将在bundle输出目录中生成webpack stats JSON文件 statsFilename: 'stats.json', // 默认值:stats.json。 如果generateStatsFile为true,将生成的webpack stats JSON文件的名称。 相对于bundle输出目录。 statsOptions: null, // 默认值:null。 stats.toJson()方法的选项。 例如,您可以使用source:false选项从stats文件中排除模块的源。 在这里查看更多选项。 logLevel: 'info' // 默认值:info, 用于控制插件输出的详细信息。 })
npm run build
image (2).png
下面是针对上述依赖图进行的优化:
在我们的项目中引入了element-ui组件库,在首屏需要加载依赖包,其中element-ui就占据了553k,原本是直接引入整个插件,会导致项目的体积太大。现在对其改造,只引入需要的组件:
1.安装babel-plugin-component:
npm install babel-plugin-component -D
修改.babelrc 文件:
{ "presets": [ ["env", { "modules": false, "targets": { "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] } }], "stage-2" ], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ]}
修改main.js文件:
// 全局引入方式,打包后会放在vendor.js文件中,在首屏加载import Vue from 'vue';import { Button } from 'element-ui';Vue.use(Button) // 单文件引入方式,打包后会放在各自路由的js文件中,跳转到具体页面才会加载对应的js文件,不会打包到vendor.js中import { Table, TableColumn, Dialog, Button } from 'element-ui'<script> export default { name: 'userCenter', components: { elTable: Table, elTableColumn: TableColumn, elDialog: Dialog, elButton: Button }}</script>
最后项目采用的是单文件引入所需要的组件,vendor.js的文件大小减小到267k
企业微信截图_16157758443453.png
项目中引用的第三方资源或者组件库很多,比如vue,vue-router,axios,swiper等等,在很多vue-处理搭建的项目都会用到,我们可以采用cdn引入,从别的服务器上加载第三方库而非自己的服务器,既能节省自己服务器的贷款,又能减少vendor.js文件的大小,会比原来webpack打包后加载快不少。
// index.html<head> <!-- 引入样式 --> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/4.5.0/css/swiper.min.css"></head><body> <div id="app></div> <!-- 引入组件库 --> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/axios/axios.min.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/4.5.0/js/swiper.min.js"></script></body>// webpack.base.conf.js 添加externals对象,告诉webpack以下第三方库不需要打包 module.exports = { ...... externals: { // 键: 库的名称, 值: 在项目中的别名, 'swiper': 'Swiper', 'vue': 'Vue', 'vue-router': 'VueRouter', 'axios': 'axios', 'element-ui': 'ELEMENT' }, }
在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。我们可以用 image-webpack-loader来压缩处理较大的图片资源:
安装 image-webpack-loader :
npm install image-webpack-loader -D
使用
// webpack.base.conf.js module: { rules: [ { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: [ { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { loader: 'image-webpack-loader',// 压缩图片 options: { mozjpeg: { // jpeg压缩 progressive: true, quality: 65 }, // optipng.enabled: false will disable optipng optipng: {//png压缩 enabled: false, }, pngquant: { // png压缩 quality: [0.65, 0.90], speed: 4 }, gifsicle: { // gif压缩 interlaced: false, } // the webp option will enable WEBP //webp: { // quality: 75 //} } } ] } ] },
使用image-webpack-loader之后处理前后的对比:
15.png
156.png
默认情况下, Babel 会在每个输出文件中内嵌一些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以使用babel-plugin-transform-runtime 插件,通过 require('babel-runtime/helpers/createClass') 的方式导入,做到只引入一次。
安装 babel-plugin-transform-runtime
npm install babel-plugin-transform-runtime --save-dev
修改 .babelrc 配置文件:
"plugins": [ "transform-runtime", [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ]]
以下是vue2.0X使用webpack打包前置帮我们安装好的插件
extract-text-webpack-plugin
: 把css代码从js文件中抽离出来,单独出一个模块
optimize-css-assets-webpack-plugin
: 压缩css文件
uglifyjs-webpack-plugin
: 压缩js文件
vue是单页面应用,而我们的网站通常又是有多个页面组成,所以会引入很多路由,如果统一都在首屏加载,那么经过webpack 打包之后文件会很大,减缓首屏加载速度,降低用户体验。因此,我们要使用路由懒加载,将不同路由对应的组件分割成不同的代码块,当路由被访问的时候才加载对应的组件。
{ path: '/index', name: '首頁', component: r => require.ensure([], () => r(require('@/page/index')), 'index'), meta: { type: 'index', title: 'XXX' }}
v-if 是 真正 的条件渲染,需要操作dom元素,有更高的切换消耗;v-show控制的元素总是会被渲染,简单地基于 CSS 的 display 属性进行切换。因此,如果需要非常频繁的切换,建议使用v-show,如果在运行时条件很少改变,则使用v-if。
在我们的数据平台或者沉淀多年的业务数据可能会有几十万,上百万条数据,这时我们出了要应用分页,无限滚动的思路,最好做窗口化渲染来优化性能,只渲染可视区域内的内容,减少重新渲染组件和创建 dom 节点的时间。具体可以参考使用vue-virtual-scroll-list 和 vue-virtual-scroller插件来实现。
在我们的项目中经常会遇到有四五个判断条件甚至更多的情况,这时如果嵌套过多过深,就会导致代码难以理解,维护困难,也会降低运行时性能。
我们可以使用return优先返回错误语句而不使用 if else模块:
if(res.code === -1) return false...... //其他需要进行的操作
也可以利用Map数据结构来判断,减少循环和更多的判断:
let map = new Map();let s = 'abbgfffklisfb'let a = 0let b = 0for(let i=0; i<s.length; i++){ if(!map.has(s[i])){ // 判断是否已经存在 a++ } else { let temp = map.get(s[i]); // 获取对应的键值 a = temp > a ? temp: a b++ } map.set(s[i], i); // 将某个字符赋予值}
在做官网或者其他视效丰富的页面时包含大量图片,如果是用PSD切下来的图直接提到线上,肯定是大大影响首屏资源加载和页面渲染的,所以我们需要对其进行压缩。推荐采用 熊猫压缩,基本上是最大程度的压缩,另外,推荐用jpg,占用内存比png格式的小。
image (3).png
项目是否需要预加载取决于开发者,用预加载一定会有一个从0到100的资源loading的过程。
<template> <div class="page-container" style="text-align: center;" > <div id="loading-panel"> <p><img src="../../static/logo.png" alt="" ></p> <h1>Loading...</h1> <h2>{{percent}}</h2> </div> </div></template><script> export default { data() { return { count: 0, percent: "", }; }, mounted() { this.preload(); console.log('hrthrth') }, created (){ let script = document.createElement('script') script.src = '../utils/' }, methods: { preload() { let imgs = [ "static/img/card1.png", "static/img/card2.png", "static/img/card3.png", "static/img/card4.png", "static/img/card5.png", "static/img/devil1.png", "static/img/devil2.png", "static/img/earth.png", "static/img/earth1.png", "static/img/earth2.png", "static/img/female-as.png", "static/img/female-de.png", "static/img/female-h.png", "static/img/404.png", "static/img/404_clond.png", "static/img/app.png", "static/img/fb.png", "static/img/bg.jpg" ]; for (let img of imgs) { let image = new Image(); image.src = img; const that = this image.onload = function(e) { that.count++; // 计算图片加载的百分数,绑定到percent变量 let percentNum = Math.floor((that.count / 14) * 100); that.percent = `${percentNum}%`; }; } } }, watch: { count: function (val) { if (val === 18) { // 图片加载完成后跳转到首页 this.$router.push({ path: "index" }); } }, }, };</script>
快捷一点的方式是使用第三方插件 Preload.js,可以预加载音视频和图片等资源。首先在index.html中引入preload.js
<script src="https://code.createjs.com/1.0.0/preloadjs.min.js"></script>
然后新建一个loading.vue文件:
<template> <div> <div id="preload_panel"> <p><img src="../../static/logo.png" alt="" ></p> <h1>Loading...{{percent}} %</h1> </div> </div></template><script> export default { name: "preload", data() { return { percent: "", }; }, mounted() { this.preLoad() }, methods: { preLoad() { var mainfest = [ { src: "static/img/card1.png" }, { src: "static/img/card2.png" }, { src: "static/img/card3.png" }, { src: "static/img/card4.png" }, { src: "static/img/card5.png" }, { src: "static/img/devil1.png" }, { src: "static/img/devil2.png" }, { src: "static/img/earth.png" }, { src: "static/img/earth1.png" }, { src: "static/img/earth2.png" }, { src: "static/img/female-as.png" }, { src: "static/img/female-de.png" }, { src: "static/img/female-h.png" }, { src: "static/img/404.png" }, { src: "static/img/404_clond.png" }, { src: "static/img/app.png" }, { src: "static/img/fb.png" }, { src: "static/img/bg.jpg" }, ]; const that = this var preload = { // 预加载函数 startPreload: function () { var preload = new createjs.LoadQueue(true); //为preloaded添加整个队列变化时展示的进度事件 preload.addEventListener("progress", this.handleFileProgress); //注意加载音频文件需要调用如下代码行 // preload.installPlugin(createjs.SOUND); //为preloaded添加当队列完成全部加载后触发事件 preload.addEventListener("complete", this.loadComplete); //设置最大并发连接数 最大值为10 preload.setMaxConnections(1); preload.loadManifest(mainfest); }, // 当整个队列变化时展示的进度事件的处理函数 handleFileProgress: function (event) { that.percent = Math.ceil(event.loaded * 100); }, // 处理preload添加当队列完成全部加载后触发事件 loadComplete: function () { that.$router.push("/index"); // 加载完成后跳转到首页 }, }; preload.startPreload(); }, }, };</script><style></style>
image (4).png
为了加速页面加载速度,我们也可以将未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。这里采用的是第三方插件vue-lazyload:
安装插件
npm install vue-lazyload
2.main.js中全局引入
import VueLazyLoad from 'vue-lazyload'// 第二个参数对象是自定义对象可有可无Vue.use(VueLazyload,{ preLoad: 1.3, error: 'dist/error.png', loading: 'dist/loading.gif', attempt: 1})
3.组件中使用,将 :src 属性直接改为v-lazy
<img v-lazy="item.src'>
1.除非首屏渲染需要用到或者是第三方埋点的sdk,其他不影响初次渲染的资源可以考虑延迟或异步加载,减少资源请求数,加快首屏渲染速度。比如FaceBook 的SDK在首页渲染时不需要用到,那我只需要在登录页面再去加载即可,在login.vue文件中:
let fbDiv = document.createElement('script')fbDiv.setAttribute('async', 'async')fbDiv.setAttribute('defer', 'defer')fbDiv.setAttribute('crossorigin', 'anonymous')fbDiv.setAttribute('src', 'https://connect.facebook.net/zh_TW/sdk.js#xfbml=1&version=v6.0&appId=xxxxxxxxx&autoLogAppEvents=1')document.querySelector('body').appendChild(fbDiv)
第三方埋点SDK如百度统计或者Google Analysics则一定要在index.html中就引入相应的代码:
<script> (function(i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function() { (i[r].q = i[r].q || []).push(arguments) }, i[r].l = 1 * new Date(); a = s.createElement(o), m = s.getElementsByTagName(o)[0]; a.async = true; a.src = g; m.parentNode.insertBefore(a, m); })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); ga( 'create', 'UA-xxxxx-x', 'auto' ); ga( 'send', 'pageview' );</script>
由于HTTP的限制,在建立一个tcp请求时会有一定的耗时,所以,我们要尽量减少请求的次数,对资源进行合并、压缩,其目的是减少http请求数和减小包体积,加快传输速度。如将项目中遇到的比较小的logo,图标等合成雪碧图,推荐合成雪碧图的在线工具:css sprites generator
image (5).png
最后总结为一下,本文由以下四个部分组成:基础Web 技术层面的优化;webpack 打包配置方面的优化;Vue 代码层面的优化;其他优化,来介绍如何优化 Vue 项目的性能。希望大家阅读完之后能有所启发,若有其他补充,欢迎交流学习!
作者:37手游技术部
链接:https://www.jianshu.com/p/cad7c8dece7d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。