使用webpack加速Web应用程序构建

随着应用程序的发展和增长,其构建时间也会增加-从开发模式的重新组装过程中的几分钟到“冷”生产组装过程中的几十分钟。 这是完全不能接受的。 我们的开发人员不喜欢在等待捆绑包准备就绪时想要切换上下文,并且希望尽早接收来自应用程序的反馈-理想情况下是从IDE切换到浏览器时。


如何实现呢? 我们该怎么做才能优化构建时间?


本文概述了webpack生态系统中现有的可加快组装速度的工具,其经验和技巧。


本文中未考虑优化包大小和应用程序本身的性能。


文本中引用了该项目,并据此进行了组装速度的测量,该项目是一个相对较小的应用程序,使用webpack,Babel,PostCSS,Sass等在JS + Flow + React + Redux堆栈上编写,约有3万个代码行和1,500个模块。 依赖版本为2019年4月的最新版本。


研究是在装有Windows 10,Node.js 8、4核处理器,8 GB内存和SSD的计算机上进行的。


术语学


  • 组装是将项目源文件转换为一起构成Web应用程序的一组相关资产的过程。
  • dev-mode-带有选项mode: 'development'组装mode: 'development' ,通常使用webpack-dev-server和watch-mode。
  • prod-mode-具有以下选项mode: 'production'组装mode: 'production' ,通常具有全套捆绑优化。
  • 增量构建-在开发模式下:仅重建有更改的文件。
  • “冷”构建-从头开始构建,没有任何缓存,但具有已安装的依赖项。

快取


缓存使您可以保存计算结果,以备将来重用。 由于缓存的开销,第一个程序集可能会比平时慢一些,但由于重用了编译未修改模块的结果,因此后一个程序集会快得多。


默认情况下,监视模式下的webpack会缓存内存中的内部生成结果,以免每次更改都不会重新组装整个项目。 对于普通版本(不在监视模式下),此设置没有意义。 您也可以尝试打开缓存解析以简化Webpack搜索模块的操作,并查看此设置是否对您的项目有明显的影响。


Webpack中没有持久性(保存到磁盘或其他存储)缓存,尽管他们承诺在版本5中添加它。 同时,我们可以使用以下工具:


-在TerserWebpackPlugin设置中缓存


默认禁用。 即使是单独使用,它也具有明显的积极作用:60.7 s→39 s(-36%),与其他缓存工具配合得很好。


开启和使用非常简单:


 optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, cache: true }) ] } 

- 缓存加载器


缓存加载器可以放置在任何加载器链中,并缓存先前加载器的结果。


默认情况下,它将缓存保存到项目根目录下的.cache-loader文件夹中。 使用加载程序设置中的cacheDirectory选项,可以重新定义路径。


用法示例:


 { test: /\.js$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: path.resolve( __dirname, 'node_modules/.cache/cache-loader' ), }, }, 'babel-loader' ] } 

安全可靠的解决方案。 它几乎可以在任何加载程序上正常工作:脚本(babel-loader,ts-loader),样式(scss-,less-,postcss-,css-loader),图像和字体(image-webpack-loader,react-svg-加载器,文件加载器)等。


请注意:


  • 当将cache-loader与style-loader或MiniCssExtractPlugin.loader结合使用时,应将其放在它们之后
    ['style-loader', 'cache-loader', 'css-loader', ...]
  • 与文档的建议相反,使用此加载器仅缓存费力的计算结果,对于“较轻”的加载器来说,它可能会带来很小但可测量的性能提升-您需要尝试并进行测量。

结果:


  • dev:35.5 s→(启用缓存加载器)→36.2 s(+ 2%)→(重组)→7.9 s(-78%)
  • 产品:60.6 s→(启用缓存加载程序)→61.5 s(+ 1.5%)→(重新组装)→30.6 s(-49%)→(打开Terser的缓存)→15, 4秒(-75%)

-HardSourceWebpackPlugin


一种更大规模,更“智能”的解决方案,用于在整个组装过程中进行缓存,而不是单个装载程序链。 在基本用例中,将插件添加到webpack配置中就足够了,标准设置对于正确的操作就足够了。 适合那些想要获得最佳性能而又不惧怕困难的人。


 plugins: [ ..., new HardSourceWebpackPlugin() ] 

文档包含使用高级设置的示例以及解决可能出现的问题的提示。 在持续运行插件之前,值得在各种情况和组装模式下彻底测试其操作。


结果:


  • dev:35.5 s→(启用插件)→36.5 s(+ 3%)→(重新组装)→3.7 s(-90%)
  • 产品:60.6 s→(打开插件)→69.5 s(+ 15%)→(重组)→25 s(-59%)→(打开Terser的缓存)→10 s(-83%)

优点:


  • 与缓存加载器相比,它可以更快地重新组装。
  • 它不需要在配置的不同位置重复声明,就像在cache-loader中一样。

缺点:


  • 与cache-loader相比,它会减慢首次构建的速度(没有磁盘缓存时);
  • 可能会稍微增加增量重建时间;
  • 在使用webpack-dev-server时可能会导致问题,并且需要对缓存分离和失效进行详细配置(请参阅文档 );
  • GitHub上的许多错误。

-在babel-loader设置中进行缓存 。 默认禁用。 其效果比高速缓存加载器要差百分之几。


-在eslint-loader设置中进行缓存 。 默认禁用。 如果使用此加载程序,则缓存将帮助您避免在重新组装过程中浪费时间排列未更改的文件。




使用cache-loader或HardSourceWebpackPlugin时,您需要禁用其他插件或加载器(TerserWebpackPlugin除外)中的内置缓存机制,因为它们在重复和增量构建中将不再有用,“冷”构建甚至会减慢速度。 如果HardSourceWebpackPlugin已经在使用,则同样适用于缓存加载器本身。




设置缓存时,可能会出现以下问题:


缓存结果应存储在哪里?


node_modules/.cache/<_>/通常存储在node_modules/.cache/<_>/目录中。 默认情况下,大多数工具都使用此路径,如果要将缓存存储在其他位置,则可以覆盖它。


什么时候以及如何使缓存无效?


对构建配置进行更改时刷新缓存非常重要,这会影响输出。 在这种情况下使用旧的缓存是有害的,并且可能导致未知性质的错误。


要考虑的因素:


  • 依赖项及其版本列表:package.json,package-lock.json,yarn.lock,.yarn-integrity;
  • 加载程序和插件显式或隐式使用的webpack,Babel,PostCSS,浏览器列表和其他配置文件的内容。

如果您不使用cache-loader或HardSourceWebpackPlugin(它们允许您重新定义源列表以形成程序集指纹),那么在添加,更新或删除依赖项时清除缓存的npm脚本将使您容易一些:


 "prunecaches": "rimraf ./node_modules/.cache/", "postinstall": "npm run prunecaches", "postuninstall": "npm run prunecaches" 

配置为在检测配置文件中的更改时清除缓存并重新启动webpack-dev-server的Nodemon也将有所帮助:


 "start": "cross-env NODE_ENV=development nodemon --exec \"webpack-dev-server --config webpack.config.dev.js\"" 

nodemon.json


 { "watch": [ "webpack.config.dev.js", "babel.config.js", "more configs...", ], "events": { "restart": "yarn prunecaches" } } 

我是否需要将缓存保存在项目存储库中?


实际上,由于缓存是程序集工件,因此不必将其提交到存储库。 缓存在node_modules文件夹中的位置(通常包含在.gitignore中)将对此有所帮助。


值得注意的是,如果有一个缓存系统可以在任何情况下(包括更改操作系统和Node.js版本)可靠地确定缓存的有效性,则缓存可以在开发机器之间或在CI中重用,这将极大地减少时间,即使在首次构建之后在分支之间切换。


在哪种构建模式下值得使用缓存,在哪些构建模式下不值得使用缓存?


这里没有明确的答案:这取决于您在开发过程中使用dev-和prod模式以及在它们之间进行切换的强度。 通常,没有什么可以阻止在任何地方打开缓存,但是请记住,它通常会减慢第一次构建的速度。 在CI中,您可能始终需要“干净”的构建,在这种情况下,可以使用适当的环境变量来禁用缓存。




有关在Webpack中缓存的有趣材料:



并行化


使用并行化,可以使用所有可用的处理器内核来提高性能。 每辆车的最终效果是不同的。


顺便说一下,这是一个简单的Node.js代码,用于获取可用的处理器内核数量(在设置下面列出的工具时可能会派上用场):


 const os = require('os'); const cores = os.cpus().length; 

-TerserWebpackPlugin设置中的并行


默认禁用。 除了自身的缓存外,它还可以轻松打开并明显加快了装配速度。


 optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, parallel: true }) ] } 

- 线程加载器


可以将线程加载器放置在执行大量计算的加载器链中,之后,之前的加载器将使用Node.js(处理器)子进程池。


它具有一组选项,可让您微调工作人员池的工作,尽管基本值看起来已经足够了。 poolTimeoutworkers应特别注意-参见示例


它可以与cache-loader一起使用,如下所示(顺序很重要): ['cache-loader', 'thread-loader', 'babel-loader'] 。 如果为线程加载器启用了“预热”,则应使用缓存仔细检查重复的程序集的稳定性-成功完成程序集后,webpack可能会挂起并且无法完成该过程。 在这种情况下,只需关闭预热。


如果在将线程加载器添加到Sass风格的编译链后遇到构建挂起的情况,则此技巧可能会有所帮助。


-happypack


一个插件,拦截加载程序的调用并将其工作分配到多个线程中。 目前,它处于支持模式(即未计划开发),其创建者建议使用线程加载器作为替代。 因此,如果您的项目保持最新状态,最好不要使用HappyPack,尽管当然值得尝试并将结果与​​线程加载器进行比较。


HappyPack拥有易于理解的配置文档 ,顺便说一句,它本身是很不寻常的:建议将加载程序配置移至插件构造函数调用,并用自己的happypack加载程序替换加载程序链本身。 在创建自定义Webpack“逐件”配置时,这种非标准方法可能会带来不便。


HappyPack支持有限数量的装载机 ; 列出了该列表中最主要和最广泛使用的内容,但由于API可能不兼容,因此无法保证其他内容的性能。 项目问题中可以找到更多信息。


拒绝计算


任何工作都需要时间。 为了减少时间,您需要避免工作量很少,可能被推迟到以后或在这种情况下根本不需要的工作。


-将装载程序应用于尽可能少的模块


测试,排除和包含属性指定加载程序在处理过程中包括模块的条件。 关键是要避免转换不需要此转换的模块。


一个流行的例子是通过Babel进行编译而产生的node_modules例外:


 rules: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' } ] 

另一个例子是普通CSS文件不需要预处理器处理:


 rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] 

-不要在开发人员模式下启用软件包大小优化


在功能强大且具有稳定Internet的开发人员计算机上,即使本地部署的应用程序重达几兆字节,也通常会快速启动。 与节省负载相比,在组装过程中优化束可以占用更多的宝贵时间。


该建议涉及JS(Terser,Uglify ),CSS(cssnano,optimize-css-assets-webpack-plugin),SVG和图像(SVGO,Imagemin,image-webpack-loader),HTML(html-minifier, html-webpack-plugin)等。


-不要在开发人员模式中包含polyfills和转换


如果使用babel-preset-env,postcss-preset-env或Autoprefixer-为开发人员模式添加单独的Browserslist配置 ,仅包括在开发过程中使用的那些浏览器。 这些很有可能是最新版本的Chrome或Firefox,可以完美地支持现代标准,而无需进行polyfill和转换。 这样可以避免不必要的工作。


示例.browserslistrc:


 [production] your supported browsers go here... [development] last 2 Chrome versions last 2 Firefox versions last 1 Safari version 

-审查源地图的使用


生成最准确和完整的源地图需要花费大量时间(在我们的项目中-使用devtool: 'source-map'选项的产品构建时间约为30%)。 考虑是否需要在prod程序集中(本地和CI中)使用源映射。 仅在必要时才值得生成它们-例如,基于提交上的环境变量或标签。


在开发模式下,大多数情况下会有一个相当轻巧的选项'cheap-eval-source-map''cheap-module-eval-source-map' 。 有关更多详细信息,请参见webpack 文档


-在Terser中设置压缩


根据Terser文档 (适用于Uglify),在compress代码时, manglecompress选项会消耗大部分时间。 通过对它们进行微调,您可以以略微增加捆束尺寸的代价实现装配加速。 vue-cli的来源中有一个示例 ,Slack的工程师中有另一个示例 。 在我们的项目中,第一个实施例中的Terser调整将组装时间减少了约7%,而束尺寸增加了2.5%。 这场比赛是否值得,取决于您。


-从解析中排除外部依赖项


使用module.noParsemodule.noParse resolve.alias您可以将库模块的导入重定向到已经编译的版本,然后将它们简单地插入捆绑中,而不会浪费时间解析。 在开发模式下,这将显着提高组装速度,包括增量速度。


该算法大致如下:


(1)列出解析时需要跳过的模块。


理想情况下,这些都是捆绑包中的所有运行时依赖项(或至少是其中最大的运行时依赖项,例如react-dom或lodash),不仅是它们自己的(第一级),而且是可传递的(依赖项依赖)。 将来,您将必须自己维护此列表。


(2)对于选定的模块,写出其编译版本的路径。


您需要为收集器提供一个替代方案,而不是跳过依赖项,并且该替代方案不应依赖于环境-调用module.exportsrequireprocessimport等。 预编译(不一定是最小化)的单文件模块通常适合于此角色,这些模块通常位于依赖项源内的dist文件夹中。 要找到它们,您必须转到node_modules。 例如,对于axios,已编译模块的路径如下所示: node_modules/axios/dist/axios.js


(3)在webpack配置中,使用resolve.alias选项将依赖关系名称的导入替换为直接导入的文件,这些文件的路径是在上一步中编写的。


例如:


 { resolve: { alias: { axios: path.resolve( __dirname, 'node_modules/dist/axios.min.js' ), ... } } } 

这里有一个很大的缺陷:如果您的代码或依赖项的代码未访问标准入口点(索引文件, package.json main字段),而是访问了依赖项源中的特定文件,或者是否将依赖项导出为ES模块,或者解决过程会干扰某些内容(例如babel-plugin-transform-imports),整个想法可能会失败。 捆绑包将组装,但应用程序将被破坏。


(4)在webpack配置中,使用module.noParse选项跳过使用正则表达式来解析步骤2的路径所请求的预编译模块。


例如:


 { module: { noParse: [ new RegExp('node_modules/dist/axios.min.js'), ... ] } } 

底线:从表面上看,该方法看起来很有希望,但存在陷阱的非平凡设置至少会增加实施成本,至少会降低收益。


具有类似操作原理的替代方法是使用externals选项。 在这种情况下,您将必须在HTML文件中独立插入指向外部脚本的链接,甚至具有与package.json对应的必需依赖项版本。


-将很少更改的代码分离到单独的包中,并且仅编译一次


当然,您听说过DllPlugin 。 使用它,您可以将活动更改的代码(您的应用程序)分发到很少的代码(例如,依赖项)到不同的程序集中。 一旦将组装的依赖项捆绑包(相同的DLL)简单地连接到应用程序组装,就可以节省时间。


大致来说是这样的:


  1. 要构建DLL,将创建一个单独的Webpack配置,并将必要的模块作为入口点进行连接。
  2. 构建从此配置开始。 DllPlugin生成DLL捆绑包和带有映射名称和模块路径的清单文件。
  3. 将DllReferencePlugin添加到将清单传递到的主程序集的配置中。
  4. 使用清单将在组装过程中DLL中呈现的依赖项的导入映射到已编译的模块上。

您可以在此处的文章中阅读更多内容。


开始使用这种方法,您将很快发现许多缺点:


  • DLL程序集与主程序集是隔离的,并且需要单独进行管理:准备特殊的配置,每次在分支切换或依赖关系更改时重新启动它。
  • 由于DLL与主装配体的工件无关,因此需要使用其他插件之一将其与其他资产一起手动复制到文件夹中,并包含在HTML文件中: 1,2
  • 必须手动更新旨在包含在DLL捆绑包中的依赖项列表。
  • 最可悲的是:摇树不适用于DLL捆绑包。 从理论上讲, entryOnly选项entryOnly目的,但他们忘记了对其进行记录。

您可以摆脱样板并使用AutoDllPlugin解决第一个问题(以及第二个问题,如果您使用html-webpack-plugin v3-不适用于版本4)。 但是,它仍然不支持“在entryOnly使用的entryOnlyentryOnly选项,并且该插件的作者对即将面世的webpack 5提出的使用其创造性思维的可行性表示怀疑。


杂项


定期更新您的软件和依赖项。 Node.js, npm / yarn (webpack, Babel .) . , changelog, issues, , .


PostCSS postcss-preset-env stage, . , stage-3, Custom Properties, stage-4 13%.


Sass (node-sass, sass-loader), Dart Sass ( Sass Dart, JS) fast-sass-loader . , . — dart-sass , node-sass, JS, libsass.


Dart Sass sass-loader . Sass fibers.


CSS-, dev-. - , , , .


一个例子:


 { loader: 'css-loader', options: { modules: true, localIdentName: isDev ? '[path][name][local]' : '[hash:base64:5]' } } 

, , : .


, - webpack PrefetchPlugin , , — . webpack issues , . ?


  1. . CLI- --json , . . , , dev- .
  2. - Hints.
  3. , “Long module build chains”. , — PrefetchPlugin .
  4. PrefetchPlugin. . StackOverflow .

: .



, (TypeScript, Angular .) — !


资料来源


, , , .


Source: https://habr.com/ru/post/zh-CN451146/


All Articles