Webpack 4进阶--从前的日色变得慢 ,一下午只够打一次包

从前的日色变得慢,车,马,邮件都慢,一辈子只够爱一我的 -- 《从前慢》css

近期在团队项目里把Webpack升级到4.4.1,过程当中发现现存的升级文档十分有限,踩了很多坑,好在升级以后提高还算显著,production场景下第三方依赖打包速度提高76%,development场景下本地服务首次启动提高效果约46%,再次启动提高效果上升至63%。这里将此次升级过程当中的点滴分享出来,但愿对你们有所帮助。html

理论部分

Webpack 4 发布以后,议论最多的两大特性,其一是零配置,其二是速度快(号称提速上限98%)。听起来十分美妙,在实地测试以前,首先从理论上分析一下可能性。react

零配置

一言以蔽之,约定优于配置。经过mode属性将开发/生产(development/production)环境中经常使用的功能设置好默认值,用户即来即用。webpack

打包速度快

Optimization

Webpack 4取消了四个经常使用的用于性能优化的plugin(UglifyjsWebpackPlugin,CommonsChunkPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin),转而提供了一个名为optimization的配置项,用于接手以上四位的工做。 git

注:UglifyjsWebpackPlugin并不执行tree shaking操做,这里为了介绍sideEffects,故而将关系紧密的二者放在一块儿介绍了

  1. Tree Shaking & Minimize

废弃插件:UglifyjsWebpackPlugingithub

新增属性:sideEffects,minimize等web

  • Tree Shaking

Tree shaking一直是一个美丽而高不可攀的话题。影响tree shaking的根本缘由在于side effects(反作用),其中最广为人知的一条side effect就是动态引入依赖的问题。得益于ES6的模块化实现思路,全部的依赖必须位于文件顶部,静态引入(然而import()的出现打破了这个规则),Webpack能够在绘制依赖图的时候进行静态分析,从而将真正被引用的exports添加到bundle文件中,减小打包体积。然而不少热度较高的第三方库为了考虑兼容性每每采用UMD实现,而其所支持的动态引入依赖的功能则致使真实的依赖图可能要到运行时才能肯定,使得静态分析难以发挥真正威力,tree shaking采用了保守策略,致使咱们发现没有被用到的方法依然出如今了bundle文件中。typescript

一个好消息是许多第三方库相继推出了es版,配合tree-shaking食用,口感更佳,这也是官方号称提速98%的重要前提之一(冷漠脸)。坏消息是ES6其实也提供import()方法支持动态引入依赖,因此如下写法其实也是彻底行的通的。。。还记得那些年咱们追过的沈佳宜说过的话么,“人生原本就有不少事情是徒劳无功的啊”。浏览器

if(Math.random() > 0.5) {
    import('./a.js').then(() => {
        ...
    })
} else {
    import('./b.js').then(() => {
        ...
    })
}
复制代码

除此之外,为了防止用户不当心修改输出元素的属性,有些库会将最终的输出元素用Object.freeze方法包裹起来,这也属于side effects之一,一样也会对tree shaking产生影响。缓存

回到Webpack 4,官方提供了sideEffects属性,经过将其设置为false,能够主动标识该类库中的文件只执行简单输出,并无执行其余操做,能够放心shaking。除了能够减少bundle文件的体积,同时也可以提高打包速度。为了检查side effects,Webpack须要在打包的时候将全部的文件执行一遍。而在设置sideEffects以后,则能够跳过执行那些未被引用的文件,毕竟已经明确标识了“我是平民”。所以对于一些咱们本身开发的库,设置sideEffects为false大有裨益。

  • Minimize

Minimize属性就没啥可多说的了,混淆压缩文件。

  1. Scope hoisting

废弃插件:ModuleConcatenationPlugin

新增属性:concatenateModules

//开启前
[
    /* 0 */
    function(module, exports, require) {
        var module_a = require(1)
        console.log(module_a['default'])
    }
    
    /* 1 */
    function(module, exports, require) {
        exports['default'] = 'module A'
    }
]

//开启后
[
    function(module, exports, require) {
        var module_a_defaultExport = 'module A'
        console.log(module_a_defaultExport)
    }
]
复制代码

concatenateModules开启以后,能够看出bundle文件中的函数声明变少了,于是能够带来的好处,其一,文件的体积比以前更小了,其二,运行代码时建立的函数做用域变少了,开销也随之变少了。不过scope hoisting的效果一样也依赖于静态分析,无奈命不禁我。

  1. Code splitting

废弃插件:CommonsChunkPlugin

新增属性:splitChunks,runtimeChunk, occurrenceOrder等

  • splitChunks

splitChunks在Webpack 4里被用于取代咱们熟悉CommonsChunkPlugin。读到这里不知道你有没有发现其中的端倪,这是否意味着DllPlugin和CommonsChunkPlugin(splitChunks)能够共存了呢?

在Webpack 4以前,二者并不能一块儿使用,缘由有二

  • 一个相对没那么重要的缘由是DllPlugin服务的目标场景是develop环境,由于第三方依赖(输出文件暂称为vendors)的变动频率较低,故而在每次启动本地服务或者rebuild的时候将第三方依赖从新打包一次其实是一种浪费。经过DllPlugin,将第三方依赖的打包过程从业务代码的打包过程当中独立出来,能够大大缩短develop环境下的启动时间。同时经过设置hash值,也能够充分的利用浏览器对这部分文件的缓存,提高加载效率。而在对加载效率更为苛刻的production环境,DllPlugin打包出的文件则稍显笨重,不少重复的内容被屡次打包进了bundle文件。在这种场景下,CommonsChunkPlugin被视为更好的选择,由于咱们不须要为打包时间操心过多,加载效率是咱们惟一须要关注的内容。因此在webpack的开发者看来,这二者如同“I have an apple,I have a pen,Ah~~ Apple pen”同样,实际上并不存在什么交集。
  • 所以也引出了两者不兼容更为重要的第二个缘由,没人实现

这块功能实际上经过CommonsChunkPlugin设置两个entry point也能够实现,一个做为业务代码的入口,一个做为vendors的入口。不过存在两个问题,第一个问题是,尽管vendors被单独设置了entry point,可是在每次启动本地服务的时候,尽管打包的结果不变,hash值不变,浏览器的缓存文件也被充分利用了,它的打包过程依然会执行,因此启动时间并不会缩短,第二个问题是,许多人在使用CommonsChunkPlugin的时候并无注意到Webpack会将runtime一块儿打包进vendors文件,因此每次启动的时候,尽管你并无修改任何第三方依赖,可是vendors文件的hash值却变了,致使浏览器缓存实际上并无被利用起来。要解决这个问题,须要配置CommonsChunkPlugin将runtime单独打包成一个文件。

然而到了Webpack 4,在CommonsChunkPlugin变成splitChunks以后,出于某些未知的缘由,二者兼容性的问题被解决了。。。Happy coding。

  • runtimeChunk

runtimeChunk之因此被单独设置为一个配置项,应该就是为了主动帮助用户避免上文所述的问题吧。

  • occurrenceOrder

occurrenceOrder应用的场景是若是不手动设置chunk的名字,而采用默认值的话,Webpack将会用更短的名字去命名引用频度更高的chunk。

  • noEmitOnErrors

废弃插件:NoEmitOnErrorsPlugin

新增属性:noEmitOnErrors

noEmitOnErrors在编译出现错误时,用来跳过输出阶段。

New Plugin

Webpack 4同时实现了一套新的plugin机制,与性能相关的改进点是消除了对arguments的滥用。如同咱们推崇开发时定义类型,从而能够避免JIT过程当中产生过多的重载函数,以及下降从新编译的几率。

实践部分

讲了这么多,最后分享一下个人实操经历。Webpack 4为用户描绘的场景当然美好,然而带来便利的同时也给开发者留下了很多麻烦。首当其冲的就是兼容性的问题,不少咱们经常使用的loader,plugin还没有对此次升级作好准备,找到合适的替代工具以及积极改造自研的工具将成为升级过程当中一场重要战役。接下来我会针对在此次项目升级中我所遇到的兼容性问题以及最终采用的解决方案作一个总结,常规的Webpack 4配置能够在官方demo 中找到答案。

  1. CommonsChunkPlugin + DllPlugin

Nothing special,主要仍是一个分类问题,如何识别存在公共依赖的第三方依赖,并将其分配到不一样的entry中。例如antd和react都依赖了react,则应该将二者分配到不一样的entry中。以及如何均匀的分配依赖到不一样的entry中,使得打包以后的每一个entry大小相近。能够说十分考验一名配置工程师的功力和对源码库的了解程度。

  1. Ts-loader 由于awesome-typescript-loader(ATL)尚未合并支持Webpack 4的pr。因此ts-loader是ts爱好者们目前最好的选择。曾经ATL之因此可以打败ts-loader,成为很多人的选择,缘由有二,其一是ATL会新开一个独立的进程执行类型检查操做,所以不会影响编译时间,其二是ts的编译结果会被缓存,rebuild场景下能够提速。目前ts-loader也已经支持这两方面功能了,因此替换时并不须要担忧。
module: {
  rule: {
    test: /\.tsx?$/,
    use: [
      'cache-loader',
      {
        loader: 'thread-loader',
        options: {
          workers: require('os').cpus().length - 1,
        }
      },
      {
        loader: 'ts-loader',
        options: {
          happyPackMode: true,
          transpileOnly: true
        }
      }
    ]
  }
}

plugins: [
  new ForkTsCheckerWebpackPlugin()
]
复制代码
  • ForkTsCheckerWebpackPlugin用于新建进程执行类型检查,为此你须要关闭ts-loader自身的类型检查功能,即设置transpileOnly为true。
  • thread-loader容许新建一个worker进程去分担一些昂贵的loader操做;cache-loader则能够将loader的运行结果缓存在本地。然而二者同时也会带来额外的开销(进程管理,I/O操做),自行评估后使用。
  1. MiniCssExtractPlugin 经过名字不难猜出它的功能,因为ExtractTextWebpackPlugin尚不支持Webpack 4,并且将来极可能被吸取为配置项,Mini-css-extract-plugin能够做为过渡期的一个选择。除了常规的css抽取合并功能外,它还会在合并时清理重复的css副本,而这也是ExtractTextWebpackPlugin还没有实现的功能,因此理论上css的打包效果更优。
  2. InlineChunkWebpackPlugin(Webpack 4还没有支持) 虽然Webpack 4还没有支持这个插件,但仍是把它加在了这里,只是由于它确实有用。上文说到经过配置runtimeChunk为true,能够将运行时打包成独立的chunk,然而这个chunk体积很小,单独占用一个http请求稍显浪费,inline显然是更好的选择。InlineChunkWebpackPlugin能够帮助咱们将指定的chunk经过inline的形式写入index.html文件。在Webpack 4尚不支持的状况下,只好在http和ctrl + a&ctrl + c&ctrl + v中选择一个更合适您口味的方法了。
  3. CleanWebpackPlugin 首先我要说明,这是一个玄学plugin,用或不用彻底取决于脸黑不黑,手脏不脏。用处就是能够在打包前清理指定目录的文件,譬如说旧的bundle文件。开始我也不信,后来的结果大家也看到了。

最后秀一下数据吧

在展现最终结果以前须要声明的一点是,因为升级Webpack的同时,还解决了诸多兼容性问题,因此最终结果的表现不管优劣,都不只仅是Webpack的功过,loader以及plugin替换带来的性能影响一样不可忽略。至于如何到达提速98%,若是全部依赖所有更新成为es版本的话。。。

  1. DllPlugin + CommonsChunkPlugin对第三方依赖打包场景(production场景) Webpack 3.8.1的打包时长为57411ms,Webpack 4的打包时长为13959ms,提高效果约76%,详情以下图所示。

    webpack3.8.1
    webpack4.4.1

  2. 本地启动(development场景) Webpack 3.8.1的启动时长(仅包含业务代码打包过程)为42890ms,Webpack 4的首次启动(cache文件还没有产生)时长为23017ms,Webpack 4的再次启动(cache文件已经存在,并不是watch模式下的rebuild场景)时长为15827ms,首次启动提高效果约46%,再次启动提高效果上升至63%,详情以下图所示。

    webpack3.8.1
    webpack4.4.1(首次启动,无缓存)
    webpack4.4.1(非首次启动,有缓存)

结束语

在不纠结到底是Webpack仍是替换loader&plugin的功劳,以及升级过程当中遭遇的懵逼,躁郁,崩溃的状况下,此次升级仍是为项目带来了正反馈。若是你也是一名追求极致开发体验的配置工程师的话,此次Webpack升级仍是值得尝试的。最后但愿文章中的内容可以有所帮助。

相关文章
相关标签/搜索