Vue性能优化详解


更新记录

  • 2023-09-13:新增 v-ifv-for 的优先级原理。

代码层面优化

v-if or v-show

在Vue.js中,v-ifv-show都是用于条件渲染的指令,它们的作用是根据条件来显示或隐藏元素。

v-if是一种惰性渲染的方式,它会根据条件决定是否在DOM中创建或销毁元素。当条件为false时,对应的元素将被完全从DOM中移除,而当条件为true时,对应的元素将被重新创建并插入DOM中。这意味着当条件频繁变化时,v-if会频繁地创建和销毁元素,对性能会有一定的影响。

v-show则是通过CSS的display属性来控制元素的显示与隐藏。当条件为false时,对应的元素会被隐藏(display: none),而当条件为true时,对应的元素会被显示(display: 根据元素原有的display属性值来确定)。这意味着无论条件如何变化,元素始终保留在DOM中,只是通过CSS的控制来显示或隐藏,对性能影响较小。

因此,尽可能使用v-show而不是v-if的原因是,当需要频繁切换显示与隐藏时,v-show的性能更好,因为元素始终存在于DOM中,不需要频繁地创建和销毁。而v-if适用于在条件较少变化或需要在条件为false时彻底从DOM中移除元素的情况下使用。

注意:v-show只是通过CSS控制元素的显示与隐藏,并不会触发元素内部的生命周期钩子函数,而v-if会在条件切换时触发元素的创建和销毁生命周期钩子函数

computed 和 watch 区分使用场景

详见Vue相关细节题| QT-7274 (qblog.top)

v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  1. v-for 遍历必须为 item 添加 key

    在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。

  2. v-for 遍历避免同时使用 v-if

    v-for 比 v-if 优先级高,每个循环项都会被处理,无论 v-if 条件是否为真。如果 v-if 条件为假,则对应的循环项会被渲染为隐藏状态,但仍然会存在于DOM中。

    如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性:

    <div v-for="item in filteredItems" :key="item.id">
      {{ item.name }}
    </div>
    computed: {
      filteredItems() {
        return this.items.filter(item => item.condition);
      }
    }

    注意:对于 key 的选择,详见:Vue核心 列表渲染和数据监视 | QT-7274 (qblog.top)

优先级原理

v-ifv-for都是vue模板系统中的指令

vue模板编译的时候,会将指令系统转化成可执行的render函数

示例

编写一个p标签,同时使用v-ifv-for

<div id="app">
    <p v-if="isShow" v-for="item in items">
        {{ item.title }}
    </p>
</div>

创建vue实例,存放isShowitems数据

const app = new Vue({
  el: "#app",
  data() {
    return {
      items: [
        { title: "foo" },
        { title: "baz" }]
    }
  },
  computed: {
    isShow() {
      return this.items && this.items.length > 0
    }
  }
})

模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数

ƒ anonymous() {
  with (this) { return 
    _c('div', { attrs: { "id": "app" } }, 
    _l((items), function (item) 
    { return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) }
}

_lvue的列表渲染函数,函数内部都会进行一次if判断

初步得到结论:v-for优先级是比v-if

再将v-forv-if置于不同标签

<div id="app">
    <template v-if="isShow">
        <p v-for="item in items">{{item.title}}</p>
    </template>
</div>

再输出下render函数

ƒ anonymous() {
  with(this){return 
    _c('div',{attrs:{"id":"app"}},
    [(isShow)?[_v("\n"),
    _l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}

这时候我们可以看到,v-forv-if作用在不同标签时候,是先进行判断,再进行列表的渲染

我们再在查看下vue源码

源码位置:\vue-dev\src\compiler\codegen\index.js

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    ...
}

在进行if判断的时候,v-for是比v-if先进行判断

最终结论:v-for优先级比v-if

长列表性能优化

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间。

那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。

export default {
	data: () => ({
		users:{}
	}),
	async created() {
		const users = await axios.get('/api/users');
		this.users = Object.freeze(users)
	}
}

还可以考虑使用 Vue.extend 来创建一个无状态组件(stateless component)。

无状态组件是指不包含任何响应式数据或方法的组件,它们只负责接收数据并展示。由于无状态组件不需要Vue来劫持数据,它们在初始化时的性能会更好。

代码示例:

// 无状态组件
const StatelessComponent = Vue.extend({
  props: ['data'],
  template: `
    <div>
      <ul>
        <li v-for="item in data" :key="item.id">
          {{ item.name }}
        </li>
      </ul>
    </div>
  `
});

// 使用无状态组件
<template>
  <div>
    <stateless-component :data="items" />
  </div>
</template>

<script>
import StatelessComponent from './StatelessComponent.vue';

export default {
  components: {
    StatelessComponent
  },
  data() {
    return {
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    };
  }
};
</script>

事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 JavaScript 内使用 addEventListener 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

created() {
	addEventListener('click', this.click, false)
},
beforeDestroy(){
	removeEventListener('click', this.click, false)
}

图片资源懒加载

路由懒加载

第三方插件的按需引入

我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:

  1. 安装 babel-plugin-component

    npm install babel-plugin-component -D
  2. 修改 .babelrc 修改为:

    {
      "presets": [["es2015", { "modules": false }]],
      "plugins": [
        [
          "component",
          {
            "libraryName": "element-ui",
            "styleLibraryName": "theme-chalk"
          }
        ]
      ]
    }
  3. 在 main.js 中引入部分组件:

    import Vue from 'vue';
    import { Button, Select } from 'element-ui';
    
     Vue.use(Button)
     Vue.use(Select)

优化无限列表性能

如果你的应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-listvue-virtual-scroller 来优化这种无限列表的场景的。

服务端渲染 SSR or 预渲染

详见Vue相关细节题| QT-7274 (qblog.top)

如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO,具体的 Vue SSR 如何实现,可以参考作者的另一篇文章《Vue SSR 踩坑之旅》。

如果你的 Vue 项目只需改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。

优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin 就可以轻松地添加预渲染 。

Webpack 层面的优化

Webpack 对图片进行压缩

在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片:

  1. 首先,安装 image-webpack-loader :

    npm install image-webpack-loader --save-dev
  2. 然后,在 webpack.base.conf.js 中进行配置:

    {
      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: {
            bypassOnDebug: true,
          }
        }
      ]
    }

减少 ES6 转为 ES5 的冗余代码

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 编译出来的代码的文件大小。

  1. 首先,安装 babel-plugin-transform-runtime

    npm install babel-plugin-transform-runtime --save-dev
  2. 然后,修改 .babelrc 配置文件为:

    "plugins": [
        "transform-runtime"
    ]

    如果要看插件的更多详细内容,可以查看babel-plugin-transform-runtime 的 详细介绍

提取公共代码

如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:

  • 相同的资源被重复加载,浪费用户的流量和服务器的成本。
  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。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']
})

如果要看插件的更多详细内容,可以查看 CommonsChunkPlugin 的 详细介绍

模板预编译

当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。

预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。

提取组件的 CSS

当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。

查阅这个构建工具各自的文档来了解更多:

优化 SourceMap

我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的。

SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 )

1.png

开发环境推荐: cheap-module-eval-source-map

生产环境推荐: cheap-module-source-map

原因如下:

  • cheap: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息;
  • module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置;
  • soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;
  • eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。

构建结果输出分析

Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer

我们在项目中 webpack.prod.conf.js 进行配置:

if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin =   require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}

执行 $ npm run build --report 后生成分析报告如下:

1.png

Vue 项目的编译优化

如果你的 Vue 项目使用 Webpack 编译,需要你喝一杯咖啡的时间,那么也许你需要对项目的 Webpack 配置进行优化,提高 Webpack 的构建效率。具体如何进行 Vue 项目的 Webpack 构建优化,可以参考作者的另一篇文章《 Vue 项目 Webpack 优化实践》

基础的 Web 技术优化

开启 gzip 压缩

gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右。

以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:

  • 安装:

    npm install compression --save
  • 添加代码逻辑:

    var compression = require('compression');
    var app = express();
    app.use(compression())
  • 重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功

    1.png

浏览器缓存

为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存),如果对缓存机制还不是了解很清楚的,可以参考作者写的关于 HTTP 缓存的文章《深入理解HTTP缓存机制及原理》,这里不再赘述。

CDN 的使用

使用 Chrome Performance 查找性能瓶颈


文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录