webpack 打包优化并无什么固定的模式,通常咱们常见的优化就是拆包、分块、压缩等,并非对每个项目都适用,针对于特定项目,须要不断调试不断优化。css
对于 webpack4,建议从零开始配置,在项目初期,使用 webpack4 默认的配置。前端
接下来,本篇文章会列出全部适用于 webpack 优化打包速度的技术方案,并给出相应的限制,请在实际项目中分状况使用。若有任何疑问,请联系瓶子君。node
优化 webpack 构建速度的第一步是知道将精力集中在哪里。咱们能够经过 speed-measure-webpack-plugin
测量你的 webpack 构建期间各个阶段花费的时间:react
// 分析打包时间 const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const smp = new SpeedMeasurePlugin(); // ... module.exports = smp.wrap(prodWebpackConfig) 复制代码
特定的项目,都有本身特定的性能构建瓶颈,下面咱们对打包的每个环节进行优化。jquery
在「窥探原理:手写一个 JavaScript 打包器」中,咱们已经介绍过,打包就是从入口文件开始将全部的依赖模块打包到一个文件中的过程,固然,在打包过程当中涉及各类编译、优化过程。webpack
打包过程当中,常见影响构建速度的地方有哪些喃?git
搜索全部的依赖项,这须要占用必定的时间,即搜索时间,那么咱们就肯定了:github
咱们须要优化的第一个时间就是搜索时间。web
webpack 根据咱们配置的 loader 解析相应的文件。平常开发中咱们须要使用 loader 对 js ,css ,图片,字体等文件作转换操做,而且转换的文件数据量也是很是大。因为 js 单线程的特性使得这些转换操做不能并发处理文件,而是须要一个个文件进行处理。正则表达式
咱们须要优化的第二个时间就是解析时间。
将全部解析完成的代码,打包到一个文件中,为了使浏览器加载的包更新(减少白屏时间),因此 webpack 会对代码进行优化。
JS 压缩是发布编译的最后阶段,一般 webpack 须要卡好一会,这是由于压缩 JS 须要先将代码解析成 AST 语法树,而后须要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,所以比较耗时,打包就容易卡住。
咱们须要优化的第三个时间就是压缩时间。
当更改项目中一个小小的文件时,咱们须要从新打包,全部的文件都必需要从新打包,须要花费同初次打包相同的时间,但项目中大部分文件都没有变动,尤为是第三方库。
咱们须要优化的第四个时间就是二次打包时间。
运行在 Node.js 之上的 webpack 是单线程模式的,也就是说,webpack 打包只能逐个文件处理,当 webpack 须要打包大量文件时,打包时间就会比较漫长。
把这个 loader 放置在其余 loader 以前, 放置在这个 loader 以后的 loader 就会在一个单独的 worker【worker pool】 池里运行,一个worker 就是一个nodeJS 进程【node.js proces】,每一个单独进程处理时间上限为600ms,各个进程的数据交换也会限制在这个时间内。
thread-loader 使用起来也很是简单,只要把 thread-loader 放置在其余 loader 以前, 那 thread-loader 以后的 loader 就会在一个单独的 worker 池(worker pool)中运行。
例如:
module.exports = { // ... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, // 建立一个 js worker 池 use: [ 'thread-loader', 'babel-loader' ] }, { test: /\.s?css$/, exclude: /node_modules/, // 建立一个 css worker 池 use: [ 'style-loader', 'thread-loader', { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]--[hash:base64:5]', importLoaders: 1 } }, 'postcss-loader' ] } // ... ] // ... } // ... } 复制代码
注意:thread-loader 放在了 style-loader 以后,这是由于 thread-loader 后的 loader 无法存取文件也无法获取 webpack 的选项设置。
官方上说每一个 worker 大概都要花费 600ms ,因此官方为了防止启动 worker 时的高延迟,提供了对 worker 池的优化:预热
// ... const threadLoader = require('thread-loader'); const jsWorkerPool = { // options // 产生的 worker 的数量,默认是 (cpu 核心数 - 1) // 当 require('os').cpus() 是 undefined 时,则为 1 workers: 2, // 闲置时定时删除 worker 进程 // 默认为 500ms // 能够设置为无穷大, 这样在监视模式(--watch)下能够保持 worker 持续存在 poolTimeout: 2000 }; const cssWorkerPool = { // 一个 worker 进程中并行执行工做的数量 // 默认为 20 workerParallelJobs: 2, poolTimeout: 2000 }; threadLoader.warmup(jsWorkerPool, ['babel-loader']); threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']); module.exports = { // ... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'thread-loader', options: jsWorkerPool }, 'babel-loader' ] }, { test: /\.s?css$/, exclude: /node_modules/, use: [ 'style-loader', { loader: 'thread-loader', options: cssWorkerPool }, { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]--[hash:base64:5]', importLoaders: 1 } }, 'postcss-loader' ] } // ... ] // ... } // ... } 复制代码
注意:请仅在耗时的 loader 上使用。
在webpack构建过程当中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中,HappyPack 可利用多进程对文件进行打包(默认cpu核数-1),对多核cpu利用率更高。HappyPack 可让 Webpack 同一时间处理多个任务,发挥多核 CPU 的能力,将任务分解给多个子进程去并发的执行,子进程处理完后,再把结果发送给主进程。
happypack 的处理思路是将原有的 webpack 对 loader 的执行过程从单一进程的形式扩展多进程模式,本来的流程保持不变。使用 HappyPack 也有一些限制,它只兼容部分主流的 loader,具体能够查看官方给出的 兼容性列表。
注意:Ahmad Amireh 推荐使用 thread-loader,并宣布将再也不继续维护 happypack,因此不推荐使用它
const path = require('path') const webpack = require("webpack"); const HappyPack = require('happypack'); // 多进程loader // node 提供的系统操做模块 const os = require('os'); // 构造出共享进程池,根据系统的内核数量,指定进程池个数,也能够其余数量 const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); const createHappyPlugin = (id, loaders) => new HappyPack({ // 用惟一的标识符 id 来表明当前的 HappyPack 是用来处理一类特定的文件 id: id, // 如何处理 .js 文件,用法和 Loader 配置中同样 loaders: loaders, // 其它配置项(可选) // 表明共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多 threadPool: happyThreadPool, // 是否容许 HappyPack 输出日志,默认是 true verbose: true // threads:表明开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数 }); const clientWebpackConfig = { // ... module: { rules: [ { test: /\.(js|jsx)$/, // 把对 .js .jsx 文件的处理转交给 id 为 happy-babel 的 HappyPack 实例 use: ["happypack/loader?id=happy-babel"], // 排除 node_modules 目录下的文件 // node_modules 目录下的文件都是采用的 ES5 语法,不必再经过 Babel 去转换 exclude: /node_modules/, } ] }, // ... plugins: [ createHappyPlugin('happy-babel', [{ loader: 'babel-loader', options: { presets: ['@babel/preset-env', "@babel/preset-react"], plugins: [ ["import", { "libraryName": "antd", "style": true }], ['@babel/plugin-proposal-class-properties',{loose:true}] ], cacheDirectory: true, // Save disk space when time isn't as important cacheCompression: true, compact: true, } }]), // ... ] } 复制代码
注意,当项目较小时,多进程打包反而会使打包速度变慢。
使用 webpack 缓存的方法有几种,例如使用 cache-loader
,HardSourceWebpackPlugin
或 babel-loader
的 cacheDirectory
标志。 全部这些缓存方法都有启动的开销。 从新运行期间在本地节省的时间很大,可是初始(冷)运行实际上会更慢。
若是你的项目生产版本每次都必须进行初始构建的话,缓存会增长构建时间,减慢你的速度。若是不是,那它们就会大大缩减你二次构建的时间。
cache-loader 和 thread-loader 同样,使用起来也很简单,仅仅须要在一些性能开销较大的 loader 以前添加此 loader,以将结果缓存到磁盘里,显著提高二次构建速度。
module.exports = { module: { rules: [ { test: /\.ext$/, use: ['cache-loader', ...loaders], include: path.resolve('src'), }, ], }, }; 复制代码
⚠️ 请注意,保存和读取这些缓存文件会有一些时间开销,因此请只对性能开销较大的 loader 使用此 loader。
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') const clientWebpackConfig = { // ... plugins: [ new HardSourceWebpackPlugin({ // cacheDirectory是在高速缓存写入。默认状况下,将缓存存储在node_modules下的目录中 // 'node_modules/.cache/hard-source/[confighash]' cacheDirectory: path.join(__dirname, './lib/.cache/hard-source/[confighash]'), // configHash在启动webpack实例时转换webpack配置, // 并用于cacheDirectory为不一样的webpack配置构建不一样的缓存 configHash: function(webpackConfig) { // node-object-hash on npm can be used to build this. return require('node-object-hash')({sort: false}).hash(webpackConfig); }, // 当加载器、插件、其余构建时脚本或其余动态依赖项发生更改时, // hard-source须要替换缓存以确保输出正确。 // environmentHash被用来肯定这一点。若是散列与先前的构建不一样,则将使用新的缓存 environmentHash: { root: process.cwd(), directories: [], files: ['package-lock.json', 'yarn.lock'], }, // An object. 控制来源 info: { // 'none' or 'test'. mode: 'none', // 'debug', 'log', 'info', 'warn', or 'error'. level: 'debug', }, // Clean up large, old caches automatically. cachePrune: { // Caches younger than `maxAge` are not considered for deletion. They must // be at least this (default: 2 days) old in milliseconds. maxAge: 2 * 24 * 60 * 60 * 1000, // All caches together must be larger than `sizeThreshold` before any // caches will be deleted. Together they must be at least this // (default: 50 MB) big in bytes. sizeThreshold: 50 * 1024 * 1024 }, }), new HardSourceWebpackPlugin.ExcludeModulePlugin([ { test: /.*\.DS_Store/ } ]), ] } 复制代码
webpack3 启动打包时加上 --optimize-minimize
,这样 Webpack 会自动为你注入一个带有默认配置的 UglifyJSPlugin 。
或:
module.exports = { optimization: { minimize: true, }, } 复制代码
压缩 JavaScript 代码须要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各类规则分析和处理 AST,致使这个过程计算量巨大,耗时很是多。但 UglifyJsPlugin
是单线程,因此咱们可使用 ParallelUglifyPlugin
。
ParallelUglifyPlugin
插件实现了多进程压缩,ParallelUglifyPlugin
会开启多个子进程,把对多个文件的压缩工做分配给多个子进程去完成,每一个子进程其实仍是经过 UglifyJS
去压缩代码,可是变成了并行执行。 因此 ParallelUglifyPlugin
能更快的完成对多个文件的压缩工做。
webpack4 中 webpack.optimize.UglifyJsPlugin
已被废弃。
也不推荐使用 ParallelUglifyPlugin,项目基本处于没人维护的阶段,issue 没人处理,pr没人合并。
webpack4 默认内置使用 terser-webpack-plugin
插件压缩优化代码,而该插件使用 terser
来缩小 JavaScript
。
所谓 terser,官方给出的定义是:
用于 ES6+ 的 JavaScript 解析器、mangler/compressor(压缩器)工具包。
为何 webpack 选择 terser?
再也不维护 uglify-es ,而且 uglify-js 不支持 ES6 +。
terser 是 uglify-es 的一个分支,主要保留了与 uglify-es 和 uglify-js@3 的 API 和 CLI 兼容性。
使用多进程并行运行来提升构建速度。并发运行的默认数量为 os.cpus().length - 1
。
module.exports = { optimization: { minimizer: [ new TerserPlugin({ parallel: true, }), ], }, }; 复制代码
能够显著加快构建速度,所以强烈推荐开启多进程
webpack 打包时,会从配置的 entry
触发,解析入口文件的导入语句,再递归的解析,在遇到导入语句时 webpack 会作两件事情:
require('react')
导入语句对应的文件是 ./node_modules/react/react.js
,require('./util')
对应的文件是 ./util.js
。以上两件事情虽然对于处理一个文件很是快,可是当项目大了之后文件量会变的很是多,这时候构建速度慢的问题就会暴露出来。 虽然以上两件事情没法避免,但须要尽可能减小以上两件事情的发生,以提升速度。
接下来一一介绍能够优化它们的途径。
使用 Loader 时能够经过 test
、 include
、 exclude
三个配置项来命中 Loader 要应用规则的文件
resolve.modules
用于配置 webpack 去哪些目录下寻找第三方模块,resolve.modules
的默认值是 ['node_modules']
,含义是先去当前目录下的 ./node_modules
目录下去找想找的模块,若是没找到就去上一级目录 ../node_modules
中找,再没有就去 ../../node_modules
中找,以此类推。
resolve.alias
配置项经过别名来把原导入路径映射成一个新的导入路径,减小耗时的递归解析操做。
在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,因此在配置 resolve.extensions
应尽量注意如下几点:
resolve.extensions
列表要尽量的小,不要把项目中不可能存在的状况写到后缀尝试列表中。有一些第三方模块会针对不一样环境提供几分代码。 例如分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在 package.json
文件里,以下:
{ "jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件 "main": "lib/index.js" // 采用 ES5 语法的代码入口文件 } 复制代码
webpack 会根据 mainFields
的配置去决定优先采用那份代码,mainFields
默认以下:
mainFields: ['browser', 'main'] 复制代码
webpack 会按照数组里的顺序去 package.json
文件里寻找,只会使用找到的第一个。
假如你想优先采用 ES6 的那份代码,能够这样配置:
mainFields: ['jsnext:main', 'browser', 'main'] 复制代码
module.noParse
配置项可让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样作的好处是能提升构建性能。 缘由是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。
// 编译代码的基础配置 module.exports = { // ... module: { // 项目中使用的 jquery 并无采用模块化标准,webpack 忽略它 noParse: /jquery/, rules: [ { // 这里编译 js、jsx // 注意:若是项目源码中没有 jsx 文件就不要写 /\.jsx?$/,提高正则表达式性能 test: /\.(js|jsx)$/, // babel-loader 支持缓存转换出的结果,经过 cacheDirectory 选项开启 use: ['babel-loader?cacheDirectory'], // 排除 node_modules 目录下的文件 // node_modules 目录下的文件都是采用的 ES5 语法,不必再经过 Babel 去转换 exclude: /node_modules/, }, ] }, resolve: { // 设置模块导入规则,import/require时会直接在这些目录找文件 // 能够指明存放第三方模块的绝对路径,以减小寻找 modules: [ path.resolve(`${project}/client/components`), path.resolve('h5_commonr/components'), 'node_modules' ], // import导入时省略后缀 // 注意:尽量的减小后缀尝试的可能性 extensions: ['.js', '.jsx', '.react.js', '.css', '.json'], // import导入时别名,减小耗时的递归解析操做 alias: { '@compontents': path.resolve(`${project}/compontents`), } }, }; 复制代码
以上就是全部和缩小文件搜索范围相关的构建性能优化了,在根据本身项目的须要去按照以上方法改造后,你的构建速度必定会有所提高。
是时候抛弃Postman了,试试 VS Code 自带神器插件👏👏👏
若是以为不错,就点个赞吧!👍👍👍
想看往期更过系列文章,点击前往 github 博客主页
❤️玩得开心,不断学习,并始终保持编码。👨💻
若有任何问题或更独特的看法,欢迎评论或直接联系瓶子君(扫码关注公众号回复 123 便可)!👀👇
👇欢迎关注:前端瓶子君,每日更新!👇