Vue性能优化
一、代码层面的优化
1、v-if
和v-for
的区别
v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
运用场景:
- 所以,
v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景; v-show
则适用于需要非常频繁切换条件的场景。
2、computed和watch区分使用场景
computed
:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch
:watch更多的进行的是观察的功能,对某个数据进行监听,在监听的数据变化时对回调进行执行操作。
运用场景:
-
当我们进行数值计算并依赖其他的数据值的时候,我们更多的是使用computed,因为我们可以利用它缓存的特性,这样避免了每次获取值的时候会对其进行多次运算;
-
当我们需要在数据变化时执行异步或者开销较大的操作时,我们更应该使用watch,使用watch选项时允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
3、v-for遍历及其与v-if的关系
v-for的遍历必须为item添加key关键字
在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。
v-for 遍历避免同时使用 v-if
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会非常影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
如:
<template>
<li
v-for="user in activeUsers"
:key="user.id">
<span>{{ user.name }}</span>
</li>
</template>
<script>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {// filter函数
return user.isActive//返回isActive为true的项,加入到activeUsers数组中
})
}
}
</script>
不推荐下面的用法:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
<span>{{ user.name }}</span>
</li>
</ul>
4、长列表性能优化
Vue 会通过 Object.defineProperty
对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,就不需要 Vue 来劫持数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间。
这时候就可以禁止 Vue 劫持了,数据可以通过 Object.freeze
方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
export default {
data: () => ({
users: {}
}),
methods:{
/*
* @params {*}
*/
async getList() {
const users = await this.$http.get("/api/users/userLists",{});
this.users = Object.freeze(users);
}
}
};
5、事件的销毁
vue组件销毁的时候,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。如果在 js 内使用 addEventListener
等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,
created() {
addEventListener('click', this.click, false)
},
beforeDestroy() {
removeEventListener('click', this.click, false)
}
另外:
单纯大量数据渲染的列表:
如果我们一次性渲染刷新几万条数据,页面会卡顿,因此只能分批渲染,既然知道原理我们就可以使用setInterval
和setTimeout
、requestAnimationFrame
来实现定时分批渲染,实现每16 ms
刷新一次
DocumentFragments
是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能
-
60HZ 刷新频率,每次刷新间隔执行一次回调函数,不会丢帧,不会卡顿
-
不会重绘或回流
const render = () => {} const id = requestAnimationFrame(render) // 60HZ 刷新频率,每次刷新间隔执行一次回调函数,不会丢帧,不会卡顿 // 不会重绘或回流 cancelAnimationFrame(id) // 取消循环
function refresh(total, onceCount) { //total -> 渲染数据总数 onceCount -> 一次渲染条数 let count = 0, //初始渲染次数值 loopCount = total / onceCount //渲染次数 function refreshAnimation() { /* * 在此处渲染数据 */ if (count < loopCount) { count++ requestAnimationFrame(refreshAnimation) } } requestAnimationFrame(refreshAnimation) }
页面例子:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>大量数据加载的例子</title> </head> <body> <div class="box"> box <div id="create"></div> <ul id="ulBox"></ul> </div> </body> <script> (function () { const ul = document.getElementById('ulBox') if(!ul){ return } const total = 100000; const once = 20; const number = total / once; var areadyNum = 0; const array = new Array(100000) //存放将要渲染的列表数据 for (var i = 0; i < array.length; i++){ array[i] = i; } function addItem() { // 创建一个虚拟节点占位 const fragment = document.createDocumentFragment(); for( var i = 0; i < once; i++){ const li = document.createElement("li") li.innerText = array[areadyNum * once + i + 1] fragment.appendChild(li); } ul.appendChild(fragment) continueDo(); areadyNum++; } function continueDo () { if(areadyNum < number){ window.requestAnimationFrame(addItem); } } continueDo(); // 点击列表项 ul.addEventListener('click', function(e) { if(e.target.tagName === "LI"){ alert(e.target.innerText) } }) })() </script> </html>
6、图片资源懒加载
对于多图片的页面,为了提升加载速度,很多时候需要将页面未显示的区域内的图片不做加载,这称为图片懒加载,等到页面到可视后进行加载,对提升性能和用户体验有较大的帮助
使用插件vue-lazyload:
- 安装:
$ npm install vue-lazyload --save-dev
- 在main.js中引入并使用:
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
- (3)在 vue 文件中将
img
标签的src
属性直接改为 指令v-lazy
,从而将图片显示方式更改为懒加载显示:
<img v-lazy="/static/img/1.png">
以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的github 地址或者npm地址。
7、路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。我们能把不同路由对应的组件分割成不同的代码块,在路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度。
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
8、按需引入第三方插件
可以,但没必要,
在实际开发中,第三方的插件基本上都会全部用到,例如ElementUi
而在这时候我们一般都会全部引入所有插件内容
如过按需引入会使得代码非常冗长。
9、优化无限列表的性能
如果应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新染组件和创建 dom
节点的时间。可以参考以下开源项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种无限列表的场景。
10、服务端渲染(SSR)和预渲染
服务端渲染
服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端的过程。
服务端渲染优点:
- 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
- 更快的内容到达时间(首屏加载更快):SPA(单页面应用) 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
服务端渲染缺点:
- 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
- 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果在高流量环境下使用,请准备相应的服务器负载,并采用缓存策略。
预渲染
如果你的 Vue 项目只需改善少数营销页面(例如 /, /about, /contact
等)的 SEO,那么你可能需要预渲染,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin 就可以轻松地添加预渲染 。
二、webpack层面优化
1、对图片进行压缩
在 vue 项目中除了可以在 webpack.base.conf.js
中 url-loader
中设置 limit 大小来对图片处理,对小于 limit (限制大小)的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader
来压缩图片:
-
安装 image-webpack-loader
$ npm install image-webpack-loader --save-dev
-
在 webpack.base.conf.js 中进行配置
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, //test的作用是检验正则表达式
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
2、减少ES6转为ES5的冗余代码ES6
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码:
class HelloWebpack extends Component{...}
这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
babel-runtime/helpers/createClass // 用于实现 class 语法
babel-runtime/helpers/inherits // 用于实现 extends 语法
在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。
为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass')
的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime
插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。
- 首先,安装
babel-plugin-transform-runtime
$ npm install babel-plugin-transform-runtime --save-dev
- 然后,修改
.babelrc
配置文件为:
"plugins": [
"transform-runtime"
]
3、提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
- 相同的资源被重复加载,浪费用户的流量和服务器的成本。
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack
内置了专门用于提取多个Chunk
中的公共部分的插件 CommonsChunkPlugin
,我们在项目中CommonsChunkPlugin
的配置如下
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module, count) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
);
}
}),
// 抽取出代码模块的映射关系
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
4、模板预编译
当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。
通常情况下这个过程已经足够快了,但对性能敏感的应用还尽量避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。
5、提取组件的CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。
构建工具各自的文档:
- webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好)
- Browserify + vueify
- Rollup + rollup-plugin-vue
6、优化 SourceMap
source-map: 一种 提供源代码到构建后代码映射 技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
source-map:外部
错误代码准确信息 和 源代码的错误位置
inline-source-map:内联
只生成一个内联source-map
错误代码准确信息 和 源代码的错误位置
hidden-source-map:外部
错误代码错误原因,但是没有错误位置
不能追踪源代码错误,只能提示到构建后代码的错误位置
eval-source-map:内联
每一个文件都生成对应的source-map,都在eval
错误代码准确信息 和 源代码的错误位置
nosources-source-map:外部
错误代码准确信息, 但是没有任何源代码信息
cheap-source-map:外部
错误代码准确信息 和 源代码的错误位置
只能精确的行
cheap-module-source-map:外部
错误代码准确信息 和 源代码的错误位置
module会将loader的source map加入
内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快
开发环境:速度快,调试更友好
速度快(eval>inline>cheap>...)
eval-cheap-souce-map
eval-source-map
调试更友好
souce-map
cheap-module-souce-map
cheap-souce-map
--> eval-source-map / eval-cheap-module-souce-map
生产环境:源代码要不要隐藏? 调试要不要更友好
内联会让代码体积变大,所以在生产环境不用内联
nosources-source-map 全部隐藏
hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
--> source-map / cheap-module-souce-map
- webpack.config.js 配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: ['./src/js/index.js', './src/index.html'],
output: {
filename: 'js/built.js',
path: resolve(__dirname, 'build')
},
module: {
rules: [
// loader的配置
{
// 处理less资源
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
// 处理css资源
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
// 处理图片资源
test: /\.(jpg|png|gif)$/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
// 关闭es6模块化
esModule: false,
outputPath: 'imgs'
}
},
{
// 处理html中img资源
test: /\.html$/,
loader: 'html-loader'
},
{
// 处理其他资源
exclude: /\.(html|js|css|less|jpg|png|gif)/,
loader: 'file-loader',
options: {
name: '[hash:10].[ext]',
outputPath: 'media'
}
}
]
},
plugins: [
// plugins的配置
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
mode: 'development',
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
hot: true
},
devtool: 'eval-source-map'
};
三、基础的Web技术优化
1、开启gzip压缩
gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。
目前主流的浏览器,Chrome,firefox,IE等都支持该协议。
常见的服务器如 Apache,Nginx,IIS 同样支持,
gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果网页有 30K,压缩之后就变成了 9K 左右-
- 安装
$ npm install compression --save
- 添加代码逻辑
var compression = require('compression');
var app = express();
app.use(compression())
- 添加成功后, response header内会添加一个请求头:
Content-Encoding:gzip
2、浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)
3、使用cdn
浏览器从服务器上下载CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。