Vue项目优化总结

3,876 阅读6分钟

Vue项目优化总结

  • 代码优化
  • Webpack配置优化
  • 其它优化

代码

v-show 和 v-if 区分使用

v-show 根据表达式之正假值,切换元素displayCSS property。当条件变化时该指令触发过渡效果。适用于频繁操作,不会触发浏览器的重排。

v-if 根据表达式的值truthiness来有条件地渲染元素。在切换时元素及它的数据绑定/组件被销毁并重建。如果是<template>元素,则提出它的内容作为条件块。当条件变化时该指令触发过渡效果。适用于不频繁操作,它会动态添加/删除节点,触发浏览器的重排。

computed 和 watch 区分使用

computed 计算属性依赖其内已被依赖的属性,结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。多用于进行数据计算、vuex数据等。

watch 当数据发生改变时会执行监听回调。多用于监听改变后执行对应操作。注意数据相互依赖且没有写好终止条件则会死循环。

隔绝观察

当复杂数据被依赖改变时,深拷贝数据进行隔绝观察,不让每次的改变都进行 UI 的重新渲染。下面是个例子:

<script>
	export default {
    data() {
      return {
        data: [
          { a: 1, b: 2, c: 3 },
          { a: 1, b: 2, c: 3 },
          { a: 1, b: 2, c: 3 },
        ]
      }
    },
    methods: {
      asnyc handle() {
        for (let i = 0; i < this.data.length; i++) {
          await this.modifyItem(this.data[i]); // 这里异步改动数据
        }
      },
      modifyItem(row) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            row.a += 1;
          }, 30);
        });
      }
    }
  }
</script>

上面代码每次异步改变数据后UI被渲染,但其实这里并不想渲染。可以将代码改成下面这样:

...
async handle() {
  const newData = JSON.parse(JSON.stringify(this.data));
  for (let i = 0; i < newData.length; i++) {
    await this.modifyItem(newData[i]); // 这里异步改动数据
  }
  // 等待数据全部修改完后再进行赋值
  this.data = newData;
}
...

大数组优化1

当前组件如果只是为纯展示组件时,拿到数据后使用Object.freeze()将数据冻结,这样数据就无法进行响应变化。

注意:冻结后无法解冻。

大数组优化2

当组件处于非常长的列表时,数据过多导致DOM元素同样多,导致卡顿。

方法1:如果是Select组件的话可以使用滚动加载配合搜索,可以看看我这篇文章的处理方式el-select数据过多懒加载

方法2:使用业界常用手段虚拟滚动,只渲染可以看到的窗口的区域DOM。参看开源项目vue-virtual-scroll-list

事件销毁

Vue 组件销毁时,实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。单独添加的监听事件是不会移除的,需要手动移除事件的监听,以免造成内存泄漏。

created() {
	document.addEventListener('scroll', this.onScroll, false);
},
beforeDestory() {
	document.removeEventListener('scroll', this.onScroll, false);
}

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效。

const Foo = () => import('./Foo.vue');
const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
});

Webpack配置优化

图片压缩

Webpack 配置中 url-loader 中设置limit来对小于limit的图片转化为 base64 格式,其余不作处理,对于这些剩余的大图在资源加载的时候会很慢,使用image-webpack-loader压缩图片。

yarn add image-webpack-loader --dev

webpack.config.js中:

rules: [{
  test: /\.(gif|png|jpe?g|svg)$/i,
  use: [
    'file-loader',
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true, // webpack@1.x
        disable: true, // webpack@2.x and newer
      },
    },
  ],
}]

按组件分割代码

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。多个小组件代码合并在一起减少请求次数。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

减少 Babel 编译后的冗余代码

Babel 插件会在代码装换生成 ES5 代码时注入一些辅助函数,例如下面的代码:

class Component1 extends CompoentBase {}

转换成浏览器可以正常执行的 ES5 代码时需要下面两个辅助函数:

babel-runtime/helpers/createClass // 实现 class 语法
babel-runtime/helpers/interits // 实现 extends 语法

默认情况下,Babel 将在每个输出的文件中加入这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数会出现多次,造成冗余。

避免这种冗余我们可以在依赖它们时通过require('babel-runtime/helpers/createClass')的方式,这样它们就只出现一次。

babel-plugin-transform-runtime插件就是实现这个作用的,将相关辅助函数替换成导入语句,解决 Babel 编译装换后的冗余。

yarn add babel-plugin-transform-runtime --dev

.babelrc中:

"plugins": [
  "transform-runtime"
]

提取公共代码

每个页面都有第三方库和公共模块,会存在下面问题:

  • 相同资源重复加载,浪费资源
  • 每个页面打开都要加载这些资源,加载时间变长

所以将公共模块代码抽离成单独的文件,Webpack 提供了相应的功能optimization.splitChunks,在webpack.config.js中:

optimization: {
    splitChunks: {
        cacheGroups: {
            default: {
                name: 'common',
                chunks: 'initial',
                minChunks: 2  //模块被引用2次以上的才抽离
            }
        }
    }
}
  • name: 提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是index~a.js这样的
  • chunks: 指定哪些类型的chunk参与拆分,值可以是string可以是函数。如果是string,可以是这个三个值之一:all, async, initialall 代表所有模块,async代表只管异步加载的, initial代表初始化时就能获取的模块。如果是函数,则可以根据chunk参数的name等属性进行更细致的筛选
  • minChunks: 模块被引用多少次以上的才抽离

提取第三方库

第三方库使用 CDN 预加载+externals+HtmlWebpackPlugin处理,有这么几点好处:

  • 不需要每个页面都去加载
  • 加快项目打包速度
  • 加快项目热更新速度

vue-cli中的配置,Webpack 配置基本一样只是写法不同,vue-cli 也是封装了 Webpack。文件vue.config.js中:

...
// 防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)
const externals = {
  vue: 'Vue',
  'vue-router': 'VueRouter',
  vuex: 'Vuex',
  // 其它自己项目中的第三方库
}
// 区分不同环境, 
// 根据自己项目不同的配置
const cdnDict = {
  dev: {
    css: [
      'https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.css',
    ],
    js: [
      'https://cdn.bootcss.com/tinymce/4.9.2/tinymce.min.js',
    ]
  },
  build: {
    css: [
      '//cdn.kuguanwang.com/jxc/public/css/nprogress.min.css'
    ],
    js: [
      '//cdn.kuguanwang.com/jxc/public/js/vue.min.js',
      '//cdn.kuguanwang.com/jxc/public/js/vue-router.min.js',
      '//cdn.kuguanwang.com/jxc/public/js/vuex.min.js',
    ]
  }
}
...
chainWebpack: config => {
  // 添加 cdn 参数到 HtmlWebpackPlugin 中,在 public/index.html 中使用
  config.plugin('html').tap(args => {
    if (process.env.NODE_ENV === 'production') {
      args[0].cdn = cdn.build;
    } else if (process.env.NODE_ENV === 'development') {
      args[0].cdn = cdn.dev;
    }
    return args;
  });
}
...
configureWebpack: config => {
  if (['production', 'development'].includes(process.env.NODE_ENV)) {
    config.externals = externals;
  }
  return config;
}

public/index.html中加入:

<head>
  ...
  <!-- 使用CDN加速的CSS文件,配置在vue.config.js下 -->
  <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style">
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
  <% } %>

  <!-- 使用CDN加速的JS文件,配置在vue.config.js下 -->
  <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="preload" as="script">
  <% } %>
  ...
</head>

提取单文件组件的CSS

在使用单文件组件时,组件内的 CSS 会议 style 标签的形式通过 JavaScript 动态注入。

这里会有一些小的运行时的开销,如果使用服务端渲染,会导致文档样式短暂失效简称为 FOUC。将所有组件的 CSS 提取到同一个文件可以避免该问题,也可以更好的进行压缩和缓存。

yarn add extract-text-webpack-plugin --dev

webpack.config.js中:

var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.export = {
  ...
  module: {
    rules: [{
      test: /\.vue$/,
      loader: 'vue-loader',
      options: {
        extractCSS: true,
      }
    }]
  },
  plugin: [
    new ExtractTextPlugin('style.css')
  ]
}

优化SourceMap

由于打包后的文件经过了压缩、合并、混淆、babel编译后的代码不利于定位分析bug。

查看文档devtool选择合适自己的可选值,不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

如果要加速打包速度可以选择生产环境时不生成 SourceMap

输出文件分析

使用webpack-bundle-analyzer可以将我们打包后的文件进行图形化的方式展示,便于分析问题。

  • 打包后的文件中都有什么文件
  • 每个文件在总大小的占比,过大的话可以针对做优化
  • 模块之间包含关系
  • 是否有重复的依赖项
  • 每个文件的大小(包含gzip等)

编译优化

上面其实已经包含了编译优化的几个内容,再补充一些其它:

优化Babel转换

babel-loader 转换文件很耗时,我们只需要让它转换必须的部分:

  • 优化正则匹配
  • 开启缓存 cacheDirectory
  • 减少包含文件 include、exclude

优化前:

{
  test: /\.js$/,
  loader: 'babel-loader',
  include: [resolve('src'), resolve('test')]
}

优化后

{
  // 只有js文件则不要写成 /\.jsx?$/,提升正则表达式性能
  test: /\.js$/,
  // 开启 babel-loader 的缓存转换
  loader: 'babel-loader?cacheDirectory',
  // 只对 src 文件夹下文件进行转换
  include: [resolve('src')]
}

优化 resolve.alias 配置

创建 importrequire 的别名,来确保模块引入变得更简单。webpack.config.js:

module.exports = {
  ...
  resolve: {
    alias: {
      '@': resolve('src'),
    }
  }
};

引入模块:

import Button from '../../../components/Button.tsx'; // 无别名
import Button from '@/components/Button.tsx'; // 使用别名

优化 resolve.modules 配置

resolve.modules 用于配置 Webpack 去哪些目录寻找第三方模块。

默认值是['node_modules'], 告诉 webpack 解析模块时应该搜索的目录。

绝对路径和相对路径都能使用,但是要知道它们之间有一点差异。

通过查看当前目录以及祖先路径(即 ./node_modules, ../node_modules 等等),相对路径将类似于 Node 查找 'node_modules' 的方式进行查找。

这里使用绝对路径,在给定目录中搜索,减少搜索步骤

mudule.export = {
  ...
  resolve: {
    modules: [path.resolve(__dirname, 'node_modules')]
  }
}

优化 resolve.extensions 配置

自动解析确定的扩展。导入文件没有文件类型后轴时,会自动带上后缀查询文件是否存在。

默认为['.wasm', '.mjs', '.js', '.json'],如果文件没有后缀则会依次找不同类型文件,到最后还没有则报错。列表越长,尝试次数越多。

所以 resolve.extensions的配置也会影响性能

优化 module.noParse 配置

防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中不应该含有 import, require, define 的调用,或任何其他导入机制。忽略大型的 library 可以提高构建性能。

webpack.config.js:

module.export = {
  ...
  module: {
    noParse: /jquery|lodash/, // 写法1 正则
    noParse: (content) => /jquery|lodash/.test(content), // 写法2 函数 返回值为 Boolean
  }
}

开启多进程

当项目代码体积越来越大之后,编译打包的时候也会越来越久,开启多进程将任务分解给多个子进程并发执行,子进程执行完后将结果再返回主进程

JS 可以使用HappyPack或者thread-loader

CSS 开启'uglifyjs-webpack-plugin'的parallel参数

使用不同环境变量和模式

vue-cli构建的(Webpack 需要自己改动)项目根目录中的下列文件来指定环境变量:

.env                # 在所有的环境中被载入
.env.local          # 在所有的环境中被载入,但会被 git 忽略
.env.[mode]         # 只在指定的模式中被载入
.env.[mode].local   # 只在指定的模式中被载入,但会被 git 忽略

一个环境文件只包含环境变量的“键=值”对:

FOO=bar
VUE_APP_SECRET=secret

例如:项目中有不同的打包环境,预发布、发布,会有不同的代码配置

根目录新增文件env.uat

NODE_ENV = production
VUE_APP_NAME = uat

package.json

...
"build": "vue-cli-server build",
"build:uat": "vue-cli-server build --mode uat",

业务代码中:

if (process.env.VUE_APP_NAME === 'uat') {
  // 执行特定处理
}

开启 Gzip

Gzip是一种压缩文件格式并且也是一个在类 Unix 上的一种文件解压缩的软件,通常指GNU计划的实现,此处的gzip代表GNU zip。也经常用来表示gzip这种文件格式。主流浏览器和常用Web服务器都支持。

yarn add compression-webpack-plugin --dev

vue.config.js

var CompressionWebpackPlugin = require('compression-webpack-plugin');
...
configureWebpack: config => {
  config.plugins.push(
      new CompressionWebpackPlugin({
          test: new RegExp('\\.(js|css)$'),
          threshold: 8192,
          minRatio: 0.8
      })
  )

nginx.conf

 #开启和关闭gzip模式
    gzip on|off;
    
    #gizp压缩起点,文件大于1k才进行压缩
    gzip_min_length 1k;
    
    # gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
    gzip_comp_level 1;
    
    # 进行压缩的文件类型。
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
    
    # nginx 对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,如果找不到则不进行压缩
    gzip_static on|off
    
    # 是否在 http header 中添加Vary: Accept-Encoding,建议开启
    gzip_vary on;

    # 设置压缩所需要的缓冲区大小,以4k为单位,如果文件为7k则申请2*4k的缓冲区 
    gzip_buffers 2 4k;

    # 设置gzip压缩针对的HTTP协议版本
    gzip_http_version 1.1;

重启 Nginx 后,可以看到 Network 响应头中Content-Encoding: gzip

静态资源CDN

内容分发网络(英语:Content Delivery Network或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、影片、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

打包后的资源要上传到 CDN 服务中,在vue.config.js中:

function getPublicPath() {
		// 根据不同环境变量处理
    if (process.env.VUE_APP_NAME === 'uat') {
        return '//cdn.example.com/';
    } else {
        return './';
    }
}
module.exports = {
  ...
  publicPath: getPublicPath(), // 部署应用包时的基本 URL

其它优化

  • iconfont 代替图片
  • 浏览缓存
  • 开启 Http2
  • 服务端渲染
  • 加入骨架屏
  • 资源预加载rel="prefetch"
  • 避免重绘、重排
  • 使用GPU

参考文献