HolyZheng / holyZheng-blog

草稿
36 stars 0 forks source link

webpack5 #46

Open HolyZheng opened 4 years ago

HolyZheng commented 4 years ago

记录一下webpack的一些常用优化手段以及webpack5的新特性。

  1. Persistent Caching. :white_check_mark: --- :heart_eyes:
  2. Automatic Node.js Polyfills Removed. :white_check_mark: --- :confused:
  3. Deterministic Chunk and Module IDs. :white_check_mark: --- :grinning:
  4. SplitChunks and Module Sizes. :white_check_mark: --- :grinning:
  5. Nested/Inner-module/CommonJs tree-shaking

Persistent Caching

webpack4时代

从webpack4开始,我们需要指定配置的 mode 为 productiondevelop。当我们指定为 production的时候,webpack会将 optimization.minimize 的值置为 true,然后使用 terser-webpack-plugin 来进行代码压缩:

optimization: {
  minimize: true
}

我们也可以覆盖webpack默认的terser-webpack-plugin的配置:

minimizer: [
  new TerserPlugin({
    cache: true,   // 开启该插件的缓存,默认缓存到node_modules/.cache中
    parallel: true,  // 开启“多线程”,提高压缩效率
    terserOptions: {
      // 其他配置项
    },
    sourceMap: true, //  生成sourceMap
    exclude: /node_modules/
  }),
],

我们也可以使用其他JavaScript压缩插件来替代 terser-webpack-plugin ,比如uglifyjs-webpack-plugin

minimizer: [
  new UglifyJsPlugin({
    parallel: true,
    cache: true,
    uglifyOptions: {
       // 其他配置项
    },
    sourceMap: true,
    exclude: /node_modules/
 }) 
]

但是官方更推荐的是terser-webpack-plugin,因为uglifyjs-webpack-plugin基于 uglifyjs,而 uglifyjs 不支持es6语法。也就是说,如果你需要压缩的代码具有es6代码的话,压缩就会失败。uglifyjs-webpack-plugin为了支持es6的压缩曾经将uglifyjs换成了uglify-es,但由于uglify-es停止了维护,所以uglifyjs-webpack-plugin又用回了uglifyjs。可以参考

无论是terser-webpack-plugin 还是uglifyjs-webpack-plugin,我们都可以开启多线程parallel以及缓存cache来提高我们的打包效率。缓存会存放到 node_module/.cache/terser-webpack-plugin 或 node_module/.cache/uglifyjs-webpack-plugin 文件夹下。每当我们的webpack配置发送改变的时候,就会重新生成缓存。

webpack5 的改变

到了webpack5 这一块并没有太大的变化,依旧是推荐使用terser-webpack-plugin来进行JS的压缩,但是我们除了可以使用插件plugin提供的cache机制外,webpack5 自身也提供了缓存机制

webpack5 为了提高打包编译的速度,添加了cache特性。在development模式下默认会设为 cache: memory;production模式下会默认会取消该配置。我们可以通过设置:

module.exports = {
  cache: {
    type: 'filesystem',
    version: 'yourVersion'
  }
}

来让webpack将缓存保存到本地硬盘中,默认保存的路径是 node_modules/.cache/webpack。这里要注意每当我们修改了webpack配置,记得更新cache的version,否则可能会出现因为重用了缓存导致配置没生效的问题。

手头上一个react技术栈,打包后业务代码大小在3m左右的项目: cache措施 webpack v4 webpack v5
TerserPlugin 提供的 cache 第一次打包 27000ms+,第二次打包 7000ms+ 第一次打包 36000ms+,第二次打包 7000ms+
webpack.cache = 'filesystem' 第一次打包 35000ms+,第二次打包 28000ms+
webpack.cache = 'filesystem' && TerserPlugin 提供的 cache 第一次打包 36000ms+,第二次打包 1000ms+

Node.js Polyfills

在webpack5之前,webpack会自动的帮我们项目引入Node全局模块polyfill。我们可以通过node配置

// false: 不提供任何方法(可能会造成bug),'empty':  引入空模块, 'mock': 引入一个mock模块,但功能很少
module.exports = {
  // ...
  node: {
    console: false,
    global: false,
    process: false,
    // ...
  }
}

但是webpack团队认为,现在大多数工具包多是为前端用途而编写的,所以不再自动引入polyfill。我们需要自行判断是否需要引入polyfill,当我们用weback5打包的时候,webpack会给我们类似如下的提示:

// 在项目中我使用到了 crypto 模块,webpack5会询问是否引入对应的 polyfill。
Module not found: Error: Can't resolve 'crypto' in '/Users/xxx/Documents/private-project/webpack/ac_repair_mobile_webpack_5/node_modules/sshpk/lib/formats'

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need these module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add an alias 'resolve.alias: { "crypto": "crypto-browserify" }'
        - install 'crypto-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.alias: { "crypto": false }

webpack5中,增加了resolve.alias配置项来告诉webpack是否需要引入对应polyfill。node配置项也做了调整。

module.exports = {
  // ...
  resolve: {
    alias: {
      crypto: 'crypto-browserify',
      // ..
    }
  },  
  node: {
    // https://webpack.js.org/configuration/node/#root
    // 只能配置这三个
    global: false,
    __filename: false,
    __dirname: false,
  }
}

也就是说到了webpack5,我们需要清楚自己的项目需要引入哪些node polyfill。更加了配置的门槛,但是减少了代码的体积

webpack5中将path、crypto、http、stream、zlib、vm的node polyfill取消后 webpack v4 webpack v5
行为 webpack尝试自动引入各个node模块的polyfill webpack默认不主动引入node模块的polyfill
措施 开发者可以使用 node: {}配置项来改成mock或不提供模块 开发者可以使用 resolve.alias配置项决定每个node模块是否引入polyfill
体验 一般不会主动配置 在打包前必须确定好用到的所有node模块是否需要引入polyfill,否则打包会被中断。同时难以提前确定哪些node模块可以省略polyfill
效果 最终js代码体积2.78M 最终js代码体积:2.17M(由于下面所说原因,不确定省略了这些polyfill后项目是否正常运行)

问题

使用webpack5后,项目运行始终报错:Uncaught ReferenceError: process is not defined. 这是因为webpack5 删减了node:{}的配置内容,并且将node.process 始终设为false。但是目前无法通过resolve.alias来给process指明polyfill。(buffer也同理)如果项目里用到了process这个node模块的话如何解决?

Deterministic Chunk and Module IDs

在日常开发中,我们会对打包出来的代码文件加上哈希后缀,以便做版本管理。同时也带来了另一个问题:哈希改变导致缓存失效。我们需要尽量减少哈希改变的情况,这就是我们一直说的“优化持久化缓存”。一个最普遍的问题就是chunkId或moduleId的改变导致文件哈希后缀的改变。(业务逻辑没有变化的情况下)

ps:chunkId:打包出来的每个文件就是一个chunk。moduleId:代码里的每一个模块都是module。webpack5之前他们都是自增id。

比如,我们项目打包结构如下图: 正常打包 当我们新增一个模块时:

// 在入口文件index.js新增了模块demo
// ...
import {a} from './demo'
console.log(a);
// ... 

添加新模块后打包

所有文件的哈希后缀都发生了改变,不符合期望,vender~xxx.js的hash不应发生变化

继续当我们新增一个入口的时候:

entry: {
   index: ['./src/index.js'],
   index2: ['./src/index2.js']
},

新增入口

同样的所有文件的哈希后缀都发生了改变,不符合期望,原有文件hash不应发生变化

webpack4可以通过设置optimization.moduleIds = 'hashed'与optimization.namedChunks=true来解决这写问题,但都有性能损耗等副作用。而webpack5 在production模式下optimization.chunkIds和optimization.moduleIds默认会设为'deterministic',webpack会采用新的算法来计算确定性的chunkI和moduleId。进而避免上述情况发生。

稳定ModuleIDs webpack v4 webpack v5
措施 optimization.moduleIds = 'hashed' 默认的 optimization.moduleIds = 'deterministic'
添加新模块时表现 只有index.xxx.js文件的hash发变化,符合预期 同左,符合预期
优缺点 将模块路径进行hash作为moduleId,该过程有一定的性能损耗(感知小)
稳定ChunkIDs webpack v4 webpack v5
措施 optimization.namedChunks=true 默认的 optimization.chunkIds = 'deterministic'; namedChunks在生产模式下被禁用
新增入口时的表现 原有文件哈希没有发生变化,符合预期(moduleIds = 'hashed'也要同时开启) 同左,符合预期
优缺点 基于NamedChunksPlugin将chunk名称作为chunkId,本意是为了开发环境更方便调试

SplitChunks and Module Sizes

在项目中有时候我们需要将一些重复的公用代码或一些异步加载的代码提取出来从而对打包出来的代码进行体积或网络缓存方面的优化。webpack3的时候,我可以通过 CommonsChunkPlugin 来进行处理:

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
     name: 'common' // 指定公共 bundle 的名称。
  })
]

到了webpack4以及webpack5,我们可以通过 SplitChunksPlugin 来处理,我们利用它来对代码进行分割:

// 默认配置
module.exports = {
  //...
  // https://github.com/webpack/changelog-v5#changes-to-the-configuration
  // https://webpack.js.org/plugins/split-chunks-plugin/
  optimization: {
    splitChunks: {
      chunks: 'async',  // 只对异步加载的模块进行处理
      minSize: 30000, // 模块要大于30kb才会进行提取
      minRemainingSize: 0, // 代码分割后,文件size必须大于该值    (v5 新增)
      maxSize: 0,
      minChunks: 1,  // 被提取的模块必须被引用1次
      maxAsyncRequests: 6, // 异步加载代码时同时进行的最大请求数不得超过6个
      maxInitialRequests: 4, // 入口文件加载时最大同时请求数不得超过4个
      automaticNameDelimiter: '~', // 模块文件名称前缀
      cacheGroups: {
     // 分组,可继承或覆盖外层配置
        // 将来自node_modules的模块提取到一个公共文件中 (又v4的vendors改名而来)
        defaultVendors: {                                                                      
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
       // 其他不是node_modules中的模块,如果有被引用不少于2次,那么也提取出来
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

SplitChunksPlugin的默认配置是经过webpack团队经过大量测试精心挑选的较为通用的配置,对于我们大部分的项目我们直接使用默认配置即可,当然为了更大程度的利用代码分割的特性,我们可以将chunks修改为 chunks: 'all',这样webpack就会对我们应用的entry points以及import() 的模块进行分割处理。

提取webpack runtime代码

另外还可以通过 optimization.runtimeChunk 配置将webpack运行时的代码提取出来,因为这部分代码理论上是不会变的。可以通过将这部分文件进行缓存。同时也可以以防万一避免这部分代码的改变导致其他提出出来的模块的哈希后缀发送变化而导致缓存失效。

optimization: {
    // ...
    runtimeChunk: true
},

最大化利用缓存

在日常开发中,我们还会给打包出来的文件加上哈希后缀,以便缓存以及版本管理。而添加哈希后缀有以下几种常规做法:

// 1. 不推荐,每个文件间的hash值存在耦合
output: {
  filename: 'js/[name].[hash].js'
  // ...
},

// 2. 可以
output: {
  filename: 'js/[name].[chunkhash].js'
  // ...
},

// 3. 推荐
output: {
  filename: 'js/[name].[contenthash].js'
  // ...
},

为了做到版本控制的同时最大化利用缓存,我们要避免每个文件之间的哈希后缀的耦合。每个文件的哈希后缀应该只与它自身的内容有关。使用 contenthash 可以在你修改了业务代码重新打包的时候,提取自node_modules以及webpack runtime的文件哈希后缀保持不变,继续利用缓存。而且在webpack5中,我们可以对 minSizemaxSize进行更详细的配置(而不是只能填一个数字),并且还可以设置js之外的文件类型的size:

splitChunks: {
    chunks: 'all',
    minSize: {
       javascript: 30000,
       style: 20000,
    }
}

tree-shaking

webpack5的tree-shaking可以涵盖更多的场景:

  1. nested-tree-shaking
  2. inner-module-tree-shaking
  3. 支持部分CommomJs 场景

在小型项目,或比较少问题的项目里感知不强。

总结:

等到webpack v5发布和相关生态跟上后,还是非常建议在新项目上去使用webpack5的。整体的效果就是:

  1. 打包速度大幅度提升 (Persistent Caching)
  2. 代码体积减少 (Automatic Node.js Polyfills Removed-主要 && Better Tree-shaking-次要)
  3. 长期缓存得到webpack默认优化,降低了对应的配置门槛(Deterministic Chunk and Module IDs)
  4. 配置门槛稍微提高,需提前自行判断是否需要引入Polyfills(Automatic Node.js Polyfills Removed)
  5. 更灵活的代码分割配置 (虽然只是灵活了一点点---minSize/maxSize的改动以及一些新字段的添加)