Skip to content
On this page

前言

Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。本文是作者通过实际项目的优化实践进行总结而来,希望读者读完本文,有一定的启发思考,从而对自己的项目进行优化起到帮助。本文内容分为以下三部分组成:

Vue 代码层面的优化;

webpack 配置层面的优化;

基础的 Web 技术层面的优化。

一、代码层面的优化

1.1、v-if 和 v-show 区分使用场景

v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

1.2、computed 和 watch 区分使用场景

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

1.3、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 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。

推荐:

ts
<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
   return user.isActive
    })
  }
}
```ts
不推荐:
```ts
<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
```ts
**1.4、长列表性能优化**

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
```ts
export default {
  data: () => ({
    users: {}
  }),
  async created() {
    const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};
```ts
**1.5、事件的销毁**

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。如果在 js 内
```ts
created() {
  addEventListener('click', this.click, false)
},
beforeDestroy() {
  removeEventListener('click', this.click, false)
}
```ts
**1.6、图片资源懒加载**

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:

(1)安装插件

npm install vue-lazyload --save-dev
ts
2在入口文件 man.js 中引入并使用

```ts
import VueLazyload from 'vue-lazyload'
```ts
然后再 vue 中直接使用


```ts
Vue.use(VueLazyload)
```ts
或者添加自定义选项
```ts
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})
```ts
3 vue 文件中将 img 标签的 src 属性直接改为 v-lazy从而将图片显示方式更改为懒加载显示



`<img v-lazy="/static/img/1.png">`
以上为 vue-lazyload 插件的简单使用如果要看插件的更多参数选项可以查看 vue-lazyload  github 地址

**1.7路由懒加载**
Vue  是单页面应用可能会有很多的路由引入这样使用 webpcak 打包后的文件很大当进入首页时加载的资源过多页面会出现白屏的情况不利于用户体验如果我们能把不同路由对应的组件分割成不同的代码块然后当路由被访问的时候才加载对应的组件这样就更加高效了这样会大大提高首屏显示的速度但是可能其他的页面的速度就会降下来

路由懒加载
```ts
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})
```ts
**1.8第三方插件的按需引入**

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

1首先安装 babel-plugin-component


```ts
npm install babel-plugin-component -D
```ts
2然后 .babelrc 修改为
```ts
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
```ts
3 main.js 中引入部分组件
```ts
import Vue from 'vue';
import { Button, Select } from 'element-ui';

 Vue.use(Button)
 Vue.use(Select)
```ts
**1.9优化无限列表性能**

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

**1.10服务端渲染 SSR or 预渲染**

服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染

1服务端渲染的优点

更好的 SEO因为 SPA 页面的内容是通过 Ajax 获取而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容 SSR 是直接由服务端返回已经渲染好的页面数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面

更快的内容到达时间首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后才开始进行页面的渲染文件下载等需要一定的时间等所以首屏渲染需要一定的时间SSR 直接由服务端渲染好页面直接返回显示无需等待下载 js 文件及再去渲染等所以 SSR 有更快的内容到达时间

2服务端渲染的缺点

更多的开发条件限制例如服务端渲染只支持 beforCreate  created 两个钩子函数这会导致一些外部扩展库需要特殊处理才能在服务端渲染应用程序中运行并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同服务端渲染应用程序需要处于 Node.js server 运行环境

更多的服务器负载 Node.js 中渲染完整的应用程序显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源因此如果你预料在高流量环境下使用请准备相应的服务器负载并明智地采用缓存策略

如果你的项目的 SEO  首屏渲染是评价项目的关键指标那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO具体的 Vue SSR 如何实现可以参考作者的另一篇文章Vue SSR 踩坑之旅》。如果你的 Vue 项目只需改善少数营销页面例如  //about/contact  SEO那么你可能需要预渲染在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件优点是设置预渲染更简单并可以将你的前端作为一个完全静态的站点具体你可以使用 prerender-spa-plugin 就可以轻松地添加预渲染



## Webpack 层面的优化

**2.1Webpack 对图片进行压缩**

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

1首先安装 image-webpack-loader
```ts
npm install image-webpack-loader --save-dev
```ts
2然后 webpack.base.conf.js  中进行配置
```ts
{
  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,
      }
    }
  ]
}
```ts
2.2减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数例如下面的 ES6 代码
```ts
class HelloWebpack extends Component{...}
```ts
这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数


```ts
babel-runtime/helpers/createClass  // 用于实现 class 语法
babel-runtime/helpers/inherits  // 用于实现 extends 语法
```ts
在默认情况下Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码如果多个源代码文件都依赖这些辅助函数那么这些辅助函数的代码将会出现很多次造成代码冗余为了不让这些辅助函数的代码重复出现可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入这样就能做到只让它们出现一次babel-plugin-transform-runtime 插件就是用来实现这个作用的将相关辅助函数进行替换成导入语句从而减小 babel 编译出来的代码的文件大小

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


```ts
npm install babel-plugin-transform-runtime --save-dev
```ts
2然后修改 .babelrc  配置文件为
```ts
"plugins": [
    "transform-runtime"
]
```ts
如果要看插件的更多详细内容可以查看babel-plugin-transform-runtime  详细介绍

**2.3提取公共代码**

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

相同的资源被重复加载浪费用户的流量和服务器的成本

每个页面需要加载的资源太大导致网页首屏加载缓慢影响用户体验

所以我们需要将多个页面的公共代码抽离成单独的文件来优化以上问题Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin我们在项目中 CommonsChunkPlugin 的配置如下

// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
```ts
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']
})
```ts
如果要看插件的更多详细内容可以查看 CommonsChunkPlugin  详细介绍

**2.4模板预编译**

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

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

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

**2.5提取组件的 CSS**

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

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

webpack + vue-loader ( vue-cli  webpack 模板已经预先配置好)

Browserify + vueify

Rollup + rollup-plugin-vue

**2.6优化 SourceMap**

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

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



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

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

原因如下

cheap源代码中的列信息是没有任何作用因此我们打包后的文件不希望包含列相关信息只有行信息能建立打包前后的依赖关系因此不管是开发环境或生产环境我们都希望添加 cheap 的基本类型来忽略打包前后的列信息

module不管是开发环境还是正式环境我们都希望能定位到bug的源代码具体的位置比如说某个 Vue 文件报错了我们希望能定位到具体的 Vue 文件因此我们也需要 module 配置

soure-mapsource-map 会为每一个打包后的模块生成独立的 soucemap 文件因此我们需要增加source-map 属性

eval-source-mapeval 打包代码的速度非常快因为它不生成 map 文件但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小但是在开发环境中可以试用下因为他们打包的速度很快

**2.7、构建结果输出分析**

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

我们在项目中 webpack.prod.conf.js 进行配置
```ts
if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin =   require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
```ts
执行  $ npm run build --report  后生成分析报告如下



**2.8、Vue 项目的编译优化**

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



## 基础的 Web 技术优化
**3.1、开启 gzip 压缩**

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

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

安装

npm install compression --save
添加代码逻辑
```ts
var compression = require('compression');
var app = express();
app.use(compression())
```ts
重启服务观察网络面板里面的 response header如果看到如下红圈里的字段则表明 gzip 开启成功


**3.2、浏览器缓存**

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

**3.3、CDN 的使用**

浏览器从服务器上下载 CSSjs 和图片等文件时都要和服务器连接而大部分服务器的带宽有限如果超过限制网页就半天反应不过来 CDN 可以通过不同的域名来加载文件从而使下载文件的并发连接数大大增加且CDN 具有更好的可用性更低的网络延迟和丢包率

**3.4、使用 Chrome Performance 查找性能瓶颈**

Chrome  Performance 面板可以录制一段时间内的 js 执行细节及时间使用 Chrome 开发者工具分析页面性能的步骤如下

打开 Chrome 开发者工具切换到 Performance 面板

点击 Record 开始录制

刷新页面或展开某个节点

点击 Stop 停止录制

## 

Released under the MIT License.