原稿:语雀 · webpack-vue_3:优化 · 原目录:Webpack › Demo › Webpack 搭建 Vue 项目 › webpack-vue_3:优化

这篇完全基于上一篇的构建结果,大家随意 DIY,了解常用的 webpack 优化手段

参考链接:三十分钟掌握Webpack性能优化

优化构建速度

首先大致总结 webpack 的打包过程

  • webpack 打包过程会从 entry point 出发,递归地解析 modules 间的依赖,同时构建构建依赖图
  • webpack 将不同的模块合并为 chunk,chunk 还能够合并成 chunk 组
  • 当必需的模块都被加载和编译完毕后,webpack 会输出 bundle 文件,其中包含了经过加载和编译的最终源文件,可以直接在浏览器中运行(关于module、chunk、bundle 的区分:webpack 中,module,chunk 和 bundle 的区别是什么?

以上大致分为搜索过程与加载编译过程,优化也可以从这两方面入手

指定搜索范围

这里主要配置 webpack resolve,告诉 webpack 从哪里寻找模块、排除哪些地方,从而提高模块搜索的效率。实际上在 webpack-vue_2 中引入 vue 时已经使用到了这种优化方式:

image.png
  • resolve.alias:使用 alias 创建了 import 时的别名,在 main.js 中使用 import Vue from 'vue' 等同于 import Vue from 'vue/dist/vue.esm.js'
  • resolve.extensions:使用 extensions 后,当导入文件没有后缀名时,webpack 会按给定后缀优先级尝试解析,尽量按照频率高低放入尽量少的后缀
image.png
  • test、exclude、include:在配置 loader 时,减少应用 loader 的模块搜索范围,比如排除 babel 对外部依赖的解析

使用 dll/cache 减少基础模块编译次数

  • 使用 DllPlugin 可以为更改不频繁的代码生成单独的编译结果,提高编译速度。其原理是把网页依赖的基础模块抽离出来打包到 dll 文件中,当需要导入的模块命中 dll 时,这个模块不再被打包,而是去dll中获取。在 4.x 版本后 webpack 打包性能提升,诸如 vue-cli 都没有再使用 dll。dll 配置些许麻烦,这里暂不使用了。
  • HardSourceWebpackPlugin 插件和 dll 思想一致,不过实现不同:它为模块提供中间缓存步骤,首次打包速度正常,之后打包则会比对 cache 中新旧 configHash,在 webpack5 中已经内置了开箱即用的 cache,因此不用额外引入该插件(而且亲测 5.x 版本时候后会报错)

使用 HappyPack 开启多进程

运行在Node.js之上的 Webpack 是单线程模型的,不能并行处理文件,HappyPack 可以将任务分解给多个工作者线程,最后将结果发给主线程,通过这种多线程的方式实现并行编译(官方文档

npm install --save-dev happypack

在配置中引入插件,同时修改 rule,需要和 happypack 中的 id 吻合

plugins: [
  //...
  new HappyPack({
    id: "babel",
    loaders: ["babel-loader?cacheDirectory"],
  }),
],
module: {
  rules: [
    {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ["happypack/loader?id=babel"],
      },
  ]
}

之后进行 build,实际测试,构建速度不减反增!因为在小型项目中,使用 happypack 进行调度的开销大于对编译过程的优化,因此甚至会增加项目的构建速度,只推荐在复杂项目中使用(参考链接:使用 happypack 提升 Webpack 项目构建速度),插件 thread-loader 与之类似


优化输出

区分当前环境

development(开发环境) 和 production(生产环境) 环境下的构建目标存在着巨大差异

  • 开发环境:强大的 source map、能实时重载的 localhost server
  • 生产环境:压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善前端加载时间

这代表不同环境下的配置必然有区别,webpack-merge 插件允许为不同环境编写独立的配置,同时提供公共配置

npm install --save-dev webpack-merge

删除原有的 webpack.config.js,创建三个新的配置文件,对应公共、开发、生成环境

// webpack.common.js
// 留下共用的插件;删除devTool、devServer项
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin"); // 引入插件
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  mode: "development",
  entry: "./src/main.js",
  plugins: [
    new HtmlWebpackPlugin({
      title: "Webpack Vue",
      template: "./public/index.html",
    }),
    new CleanWebpackPlugin(),
    new VueLoaderPlugin(),
  ],
  module: { /* 未改变 */ },
  resolve: { /* 未改变 */},
};
// webpack.dev.js// 指定mode;填写devTool、devServer项const { merge } = require("webpack-merge");const common = require("./webpack.common.js");module.exports = merge(common, {  mode: "development",  devtool: "inline-source-map",  devServer: {/* 未改变 */},  },});
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
})

之后修改 npm script,npm run build 用于构建生产版本,npm run serve 构建开发环境版本

"scripts": {
  "build": "webpack --config webpack.prod.js",
  "serve": "webpack serve --config webpack.dev.js"
},

代码压缩

压缩JS:Webpack 内置 UglifyJS 插件、ParallelUglifyPlugin 开启并行压缩

UglifyJS 会分析 JS 语法树,理解代码的含义,从而做到去掉无效代码、去掉日志输入代码、缩短变量名等优化。常用配置参数如下:

const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
//...
plugins: [
    new UglifyJSPlugin({
        compress: {
            warnings: false,  //删除无用代码时不输出警告
            drop_console: true,  //删除所有console语句,可以兼容IE
            collapse_vars: true,  //内嵌已定义但只使用一次的变量
            reduce_vars: true,  //提取使用多次但没定义的静态值到变量
        },
        output: {
            beautify: false, //最紧凑的输出,不保留空格和制表符
            comments: false, //删除所有注释
        }
    })
]

压缩 ES6:uglifyjs-webpack-plugin

压缩源文件中的 js 代码,且该插件提供了压缩 ES6 代码的功能

压缩 CSS:CssMinimizerWebpackPlugin

该插件使用了 cssnano,不仅是删掉空格,还能理解代码含义,例如把 color:#ff0000 转换成 color:red

使用 Tree-shaking 剔除冗余代码

Tree Shaking 可以剔除未被引用的 dead code,它依赖 ES6 模块语法的静态结构特性(在导入、导出语句中的路径必须是静态字符串,且不能放入其他代码块中),适合用于Lodash、utils.js等工具类较分散的文件。

具体使用见:Tree Shaking

使用 externals 配置常用库

打包后的 bundle 可以在真正运行时才去获取外部依赖,此时构建后的包体积就会明显缩小,提升构建和加载速度。webpack externals 配置选项提供了从输出的 bundle 中排除依赖的方法,而是在运行时获取。

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous">
</script>

典型的场景就是如上通过 CDN 引入 jQuery,此时不应该再打包模块,因此在 externals 中声明这是一个外部扩展

// webpack.config.js
module.exports = {
  //...
  externals: {
    jquery: 'jQuery'
  }
};

在其他模块中正常使用扩展即可

import $ from 'jquery';
$('.my-element').animate(/* ... */);

优化前端性能

注意,上一节的区分环境、代码压缩、tree-shaking、externals 都可优化前端性能,只不过这些优化大多在打包完成输出后就完成,而本节的优化方案侧重于前端访问时利用缓存机制、懒加载机制提高加载速度

提取公共代码

大型网站通常由多个页面组成,每个页面都是一个独立的单页应用,多个页面间肯定会依赖同样的样式文件、技术栈等。如果不把这些公共文件提取出来,那么每个单页打包出来的 bundle 中都会包含公共代码,相当于要传输 n 份重复代码。如果把公共文件提取出一个文件,那么当用户访问了一个网页,加载了这个公共文件,再访问其他依赖公共文件的网页时,就直接使用文件在浏览器的缓存,这样公共文件就只用被传输一次。

常见的提取公共代码实现: 2. 入口分离

文档给出了一个场景,分离 app 和 vendor 的入口,前者可能被频繁的维护,而后者可以是的第三方库或文件,长期不会更新,如此分割后,每次打包生成的 app bundle 哈希值会更改,浏览器会拉取最新的内容;而独立的 vendor bundle 文件名不变,浏览器可使用缓存,减少了加载时间