Vue项目优化总结

2次阅读

共计 8888 个字符,预计需要花费 23 分钟才能阅读完成。

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

参考文献

  • webpack 优化之玩转代码分割和公共代码提取
  • Vue 项目性能优化 — 实践指南(网上最全 / 详细)
正文完
 0