众所周知,Vue帮我们做了很多"脏活累活",例如:数据的双向绑定;Virtual Dom技术等,我们可以把大部分的时间抽出来去实现我们的业务逻辑。但是,我们仍然需要关注vue的首屏优化,webpack的配置优化,资源loading的快慢,vue项目运行时的性能优化等等。为了更好地提升我们的网站用户体验,下面我将通过实际项目的优化实践分模块总结:
- 基础web技术层面的优化
- webpack打包配置方面的优化
- vue代码层面的优化
- 其他优化
为了更了解整个性能优化过程,我们先来梳理一件事: 从输入URL 到页面加载完成发生了什么事?
- 用户输入URL
- 浏览器先检查本地是否有对应的IP地址,若找到则返回对应的IP地址。若没找到则请求上级DNS服务器,直至找到或到根节点
- TCP连接进行三次握手
- 浏览器发送HTTP请求资源/数据
- 服务端处理请求进行回应
- 浏览器接收HTTP响应
- 浏览器渲染页面,构建DOM树
- 浏览器关闭TCP连接(四次挥手)
从以上过程可以看出整个处理响应过程其实是三部分: 客户端请求,服务端响应,客户端接收响应,如此可以发现前端能做的优化其实是第一和第三部分:让客户端做出更有效且高效的请求,让客户端接收响应后更快速的渲染页面,实现功能。下面我们具体分析如何进行优化:
基础web技术层面的优化
页面渲染性能的优化
以下是浏览器渲染页面的过程:
- 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压缩
gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右。重启服务之后可以看到:
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了
HTTP缓存
缓存的目的是简化资源的请求路径,比如某些静态资源在客户端已经缓存了,再次请求这个资源,只需要使用本地的缓存,而无需走网络请求去服务端获取。具体的缓存规则服务器会将其放入http响应报文的response headers中与请求结果一起返回给浏览器。
缓存类型:
缓存过程:
CDN使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率
使用 Chrome Performance 查找性能瓶颈
chrome开发者工具性能分析工具 Performance 可以帮助我们监控并分析页面的性能情况,进而去采取对应的优化措施:
- 打开 Chrome 开发者工具,切换到 Performance 面板
- 点击 Record 开始录制
- 刷新页面或展开某个节点
- 点击 Stop 停止录制
webpack打包配置方面的优化
ebpack-bundle-analyzer:查看资源树 方便后续针对性的优化
- 安装
npm i webpack-bundle-analyzer -D
- 使用
//在webpack.prod.conf.js文件中加入以下代码
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
new 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
下面是针对上述依赖图进行的优化:
第三方插件的按需引入
在我们的项目中引入了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
使用 CDN加载外部资源
项目中引用的第三方资源或者组件库很多,比如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之后处理前后的对比:
减少 ES6 转为 ES5 的冗余代码
默认情况下, 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代码层面的优化
路由懒加载
vue是单页面应用,而我们的网站通常又是有多个页面组成,所以会引入很多路由,如果统一都在首屏加载,那么经过webpack 打包之后文件会很大,减缓首屏加载速度,降低用户体验。因此,我们要使用路由懒加载,将不同路由对应的组件分割成不同的代码块,当路由被访问的时候才加载对应的组件。
{
path: '/index',
name: '首頁',
component: r =>
require.ensure([], () => r(require('@/page/index')), 'index'),
meta: {
type: 'index',
title: 'XXX'
}
}
v-if和v-show的应用
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 = 0
let b = 0
for(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格式的小。
图片资源预加载
项目是否需要预加载取决于开发者,用预加载一定会有一个从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>
图片资源懒加载
为了加速页面加载速度,我们也可以将未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。这里采用的是第三方插件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.greatytc.com/analytics.js', 'ga');
ga( 'create', 'UA-xxxxx-x', 'auto' );
ga( 'send', 'pageview' );
</script>
- 由于HTTP的限制,在建立一个tcp请求时会有一定的耗时,所以,我们要尽量减少请求的次数,对资源进行合并、压缩,其目的是减少http请求数和减小包体积,加快传输速度。如将项目中遇到的比较小的logo,图标等合成雪碧图,推荐合成雪碧图的在线工具:css sprites generator
最后总结为一下,本文由以下四个部分组成:基础Web 技术层面的优化;webpack 打包配置方面的优化;Vue 代码层面的优化;其他优化,来介绍如何优化 Vue 项目的性能。希望大家阅读完之后能有所启发,若有其他补充,欢迎交流学习!