一、冷启动一个大型老项目要多久?

视频地址一

备用地址(链接: https://pan.baidu.com/s/16u0A9wBPq1cuKVUWXamxNA 提取码: juwa )

再来看一个更夸张的:
69ce51d7-e109-4655-8766-e0a4f6c12a7f.png

二、缘起

老项目由于使用的webpack版本较老,而且页面又较多,而且对于性能优化方面并没有深入地进行挖掘。导致每次开发老项目都很痛苦,光启动每次就要花费至少一分钟。

于是就想着使用最新的工具vite进行提速,不过在尝试了一番后,发现vite貌似不支持react15版本(jsx-runtime的破坏性改动,官方的vite-react插件暂未支持低版本reac),而且vite也不支持命名不为.module.xx的样式文件
看源码:
image.png
(这个问题,可以通过自己写一个插件解决,思路就是在rollup的transform阶段,ast语句分析importdecalation语句,手工替换文件后缀名解决),因为vite用户的插件是在vite构建插件之前运行的,所以可以生效。

代码示例:

  1. import { transformSync } from '@babel/core';
  2. const cssLangs = `\\.(css|less|sass|scss)($|\\?)`;
  3. const importRE = /import\s+([\S]+)\s+from\s+('|")([\S]+)\.(css|less|sass|scss)(\?[\S]*)?('|")/;
  4. const jsRE = /\.(js|ts|jsx|tsx)/;
  5. const cssLangRE = new RegExp(cssLangs);
  6. const cssModuleRE = new RegExp(`\\.module${cssLangs}`);
  7. function autoCSSModulePlugin() {
  8. return () => {
  9. return {
  10. visitor: {
  11. ImportDeclaration: (path) => {
  12. const { node } = path;
  13. if (!node) {
  14. return;
  15. }
  16. // 如果不是module css的那么就通过 转化为 module.styl来模块化css
  17. if (
  18. node.specifiers &&
  19. node.specifiers.length > 0 &&
  20. cssLangRE.test(node.source.value) &&
  21. !cssModuleRE.test(node.source.value)
  22. ) {
  23. const cssFile = node.source.value;
  24. node.source.value = cssFile + (cssFile.indexOf('?') > -1 ? '&' : '?') + '.module.less';
  25. }
  26. },
  27. },
  28. };
  29. };
  30. }
  31. function transform(code, { sourceMap, file }) {
  32. const parsePlugins = ['jsx'];
  33. if (/\.tsx?$/.test(file)) {
  34. parsePlugins.push('typescript');
  35. }
  36. const result = transformSync(code, {
  37. configFile: false,
  38. parserOpts: {
  39. sourceType: 'module',
  40. allowAwaitOutsideFunction: true,
  41. plugins: parsePlugins,
  42. },
  43. sourceMaps: true,
  44. sourceFileName: file,
  45. inputSourceMap: sourceMap,
  46. plugins: [autoCSSModulePlugin()],
  47. });
  48. return {
  49. code: result.code,
  50. map: result.map,
  51. };
  52. }
  53. export default function viteTransformCSSModulesPlugin() {
  54. const name = 'vite-plugin-transform-css-modules';
  55. return {
  56. name,
  57. transform(code, id) {
  58. if (jsRE.test(id) && importRE.test(code)) {
  59. const result = transform(code, {
  60. file: id,
  61. sourceMap: this.getCombinedSourcemap(),
  62. });
  63. if (result) {
  64. return result;
  65. }
  66. }
  67. return undefined;
  68. },
  69. };
  70. }

而且发起请求数也较多,尤其是首屏渲染时特别慢。不像webpack将多个模块合并成一个bundle,减少了请求。

image.png

这个原因一部分是因为我们的项目组件拆的很多,还有一个原因是用了antd的按需加载使用了less文件,在页面的加载的时候vite其实还是需要去编译less文件。

webpack 的打包模式在项目本身源码模块数量极大 (>1000) 的情况下还是有一点优势的,因为浏览器在处理这个级别的并发请求上会产生阻塞(但通常来说如果你一个路由下模块数到这个级别说明你代码分割/按需加载没做好)

二、预备知识

(一)esbuild

为什么我要提esbuild,因为vite使用了esbuild构建,而且webpack也可以使用esbuild-loader进行提速。

ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。

为什么这么快?

  1. js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
  2. go是纯机器码,肯定要比JIT快
  3. 不使用 AST,优化了构建流程(也带来了一些缺点,一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中,如果你的项目使用了 babel-plugin-import, 或者一些自定义的 babel-plugin ,无法使用)

四、webpack性能分析

1、webpack-bundle-analyzer

使用bundle-analyzer分析工具分析包的大小:
就拿其中路由比较少的一个项目admin-crm-web来启动,也有80多个路由:
image.png
image.png
image.png
image.png
启动时间1分半,占用内存6.77G,所有的chunk加起来有100MB

就算手动选择某个路由,仍需10几秒左右的时间。

2、speed-measure-webpack-plugin

使用speed-measure-webpack-plugin进行分析,测量各个插件和loader所花费的时间:
image.png
image.png

五、优化方案

4、浏览器驱动路由切割编译

image.png
之前的项目如果要启动的话,我们的插件falcon-webpack为了减少构建时候的打包路由,让我们进行了手动选择,这样做还是不够方便:
1、路由太多不方便选择
2、选择错了,需要关闭然后重启再次选择

想想vite是怎么做的,vite启动的时候,没有做任何的编译,等你访问页面的时候,通过浏览器的esm请求资源,vite然后根据你需要的资源进行按需编译,返回给浏览器。

为了改进使用体验,提示开发效率,我在想,能不能启动的时候不选择路由,当我浏览器访问页面的时候,再告诉webpack我需要打包这个页面,webpack然后再去打包当前页面呢?

视频地址:视频1

(备用地址 链接: https://pan.baidu.com/s/1OW7xT-RelfS3UkO_FmNu1g 提取码: 178r 复制这段内容后打开百度网盘手机App,操作更方便哦)

我的思路是,在webpack开发时候,打包空路由,然后在页面中注入脚本,监听路由变化,发送请求到webpack,通过webpack-dev-server的before钩子监听请求,获取路由变化的参数,然后在动态写入路由。
下面是我的实现:
image.pngimage.png

可以提升的点:
1、打包的时候页面一片空白,可以加入loading或者打包进度输出
2、需要一份.routes.ts文件,这份文件和你的的路由文件是相同的,只是过滤后的文件,可以用babel重写,在importDecleration中进行匹配,在内存中进行过滤

2、UglifyJsPlugin 开启parallel

利用多核处理器进行并行处理。原理就是使用nodejs启用了新的子进程。
不过后来我升级了webpack版本到5,使用了terser-plugin,也是一样的。

3、dll插件

dll和external的功能是一样的,不过是对本地的依赖进行预打包,然后再排除,如果依赖变化了,还需要再重新打包一次。
image.png
dll插件不仅可以生产环境用,构建时也能用,所以可以放开。所以我把已经生成好的dll文件放在了本地目录下,
image.png
并设置了
image.png
然后在页面引入
image.png

4、升级webpack

检查过期的包:
image.png
升级和webpack相关的包:
image.png
升级完以后再安装webpack-cli:
image.png

5、使用esbuild

不过esbuild-loader也有缺点:

(1)不支持装饰器

使用oneOf对单文件采用单一loader, 当returen false的时候即采用babel-loader + ts-loader形式打包文件;
image.png

判断输入文件是否有装饰器:

  1. /** 判断是否具有装饰器 */
  2. function hasDecorator(fileContent, offset = 0) {
  3. const atPosition = fileContent.indexOf('@', offset);
  4. if (atPosition === -1) {
  5. return false;
  6. }
  7. if (atPosition === 1) {
  8. return true;
  9. }
  10. if (["'", '"'].includes(fileContent.substr(atPosition - 1, 1))) {
  11. return hasDecorator(fileContent, atPosition + 1);
  12. }
  13. return true;
  14. }

(2)不支持按需加载

开发环境使用esbuild-loader,antd全量引入即可

(3)只能打包成es6

image.png

但是我们的项目一般都是打包成es5的,所以会存在冲突,所以可以在开发环境,设定ts-loader的configfile
image.png

8、exclude改为include

这样能将 loader 应用于最少数量的必要模块
image.png
改为
image.png

9、尽量少地使用工具

每个额外的 loader/plugin 都有其启动时间。
image.png
webpack - 图29
例如一些插件,在生产环境才需要,开发环境可以不使用例如:BannerPlugin、ExtractTextPlugin、UglifyJsPlugin等

10、Devtool

需要注意的是不同的 devtool 设置,会导致性能差异。

  • “eval” 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
  • 使用 eval-source-map 变体配置进行增量编译。

在大多数情况下,最佳选择是 eval-cheap-module-source-map

11、开发环境避免额外的优化步骤

Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:
image.png

12、开发环境输出结果不携带路径信息

Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:
image.png

13、开发环境ts-loader的优化

你可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。
image.png

14、happypack或者thread-loader

webpack3可以使用happypack,webpack5使用thread-loader,不过也是有一些限制的:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。
请仅在耗时的操作中使用此 loader!
webpack - 图33
可以看到耗时的几个loader,所以进行配置:
image.png
又遇到了坑:
image.png
主要原因就是项目中js文件使用了ts的语法,导致我不得不也用了ts-loader将js文件也进行了转换,这样的做法是不严谨的,但是老项目也没有办法。

15、noParse

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 webpack 会引入这些模块,但是不进行转化和解析,从而提升 webpack 的构建性能 ,例如:jquery 、lodash。
image.png

16、externals

已经使用了dll排除了一些第三方依赖,但是每个具体的项目还有一些外部依赖也可以排除,在不重现打包dll文件的情况下,可以使用externals。
image.png[

](https://segmentfault.com/a/1190000011795931)

17、优化 resolve.extensions 配置

在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:

  • resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
  • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。

image.png
移除.web.js

18、开启缓存

例如babel-loader开启缓存,第二次构建时会读取缓存:
image.png
webpack自带的cache不用配置,默认开发环境开启。

HtmlWebpackPlugin、TerserPlugin、eslint-loader、babel-loader统统加上缓存

除了这些之外,还有cache-loader、HardSourceWebpackPlugin都是可以开启缓存的。我这里没做尝试,大家可以有兴趣试一试。

19、webpack编译两次

image.png
在使用webpack3的时候,经常会出现编译2次的情况,好像是低版本的html-webpack-plugin才有的问题,在我升级完以后,这个问题已经不存在了。

[

](https://webpack.docschina.org/guides/build-performance/#incremental-builds)

六、踩坑

坑1:

image.png将dll的json中的meta替换为buildMeta即可。

坑2:

image.png
jsonpFunction 替换为chunkLoadingGlobal

坑3:

image.png
image.png
将loader替换为use即可。

坑4:

image.png
原因是因为falcon插件:
image.png
webpack的api变化,应该改写为
image.png

坑5:

image.png

image.png
改为:
image.png

坑6:

image.png
contentBase改为static

坑7:

image.png
disableHostCheck改为allowedHosts:’all’

坑8:

image.png
改为logging:’warn’

坑9:

image.png
webpack.optimize.CommonsChunkPlugin 升级到splitChunks
image.png

坑10:

image.png
将devserver的before换成onBeforeSetupMiddleware
image.png

坑11:

image.png
babel.rc文件中plugin不允许使用数组的配置了
image.png

坑12:

image.png
主要原因还是babel的预设都已经改为了@babel/preset-es2015这样的形式
image.png
image.png
改为安装新的预设包,并改写配置项:
image.png

坑13:

image.png
全局安装webpack-cli

坑14:

image.png
移除babel配置中的,并将@babel/preset-es2015和@babel/preset-stage-0换成@babel/preset-env
image.png

坑15:

image.png

将之前的写法改为
image.png
image.png

坑16:

image.png
改写为
image.png

坑17:

image.png
使用mini-css-extract-plugin代替extract-text-webpack-plugin即可。
image.pngimage.png
改写为:
image.pngimage.png

坑18:

image.png
对ossPlugin进行升级。

坑19:

image.png
安装 @babel/plugin-proposal-decorators插件即可。
image.png

坑20:

image.png
因为使用了babel-plugin-import,所以这个ts-import-plugin是多余的,可以去掉。
image.png

坑21:

image.png
image.png
这些报错主要是因为在js中使用了ts的功能,所以
image.png
js文件也应该用ts-loader

坑22:

image.png
类型断言只能在ts文件中使用

坑23:

image.png
zlib找不到,原因是webpack5以前是内置了nodejs的一些polyfills的,现在需要自己单独安装配置。
image.png

坑24:

image.png
还是因为装饰器的转换插件丢失了descriptor,
主要原因还是在我们的项目中,装饰器的使用的方法是
image.png
而官方的使用方法是:
image.png
在我们的项目中装饰的是class的propotery,而不是class method,所以最后根本没有走装饰器插件那里的转换。
image.png
装饰器代码注释了以后,页面正常,说明已经是最后一个坑了。image.png
这里只有2个解决办法,一个就是将项目中的代码改为和官方一样的写法,还有一个就是自己再把以前老的装饰器的插件对于class的propetry那块拿来重新一个自定义插件。

坑25:

image.png
uglifyjs-webpack-plugin改为terser-webpack-plugin
image.png

坑26:

image.png
安装配置assert
image.png

坑27:

image.png
安装配置stream-browserify
image.png

坑28:

image.png
加入ignoreOrder
image.png

坑29:
image.png
使用了esbuild之后,虽然速度提升了好几倍,
image.png
但是在webpack中配置的reslove的模块找不到引用了
image.png
image.png
image.png
因为这里项目中也是在babel中改变引用了路径的,要修复的话,必须要把crm-comps下面src的文件新建一个index.js暴露出来即可。

坑29:

image.png
image.png
主要是main设置的有问题,项目中是通过babel改变了引入的目录的,现在使用esbuild就会报错
image.png
改为:
image.png

七、优化效果

image.png
全部启动只花了不到10秒,还记得前面没有优化之前启动一个整个项目需要多久吗?1份10几秒,时间提升了90%!

这里
image.png

一、冷启动一个大型老项目要多久?

视频地址一

备用地址(链接: https://pan.baidu.com/s/16u0A9wBPq1cuKVUWXamxNA 提取码: juwa )

再来看一个更夸张的:
69ce51d7-e109-4655-8766-e0a4f6c12a7f.png

二、缘起

老项目由于使用的webpack版本较老,而且页面又较多,而且对于性能优化方面并没有深入地进行挖掘。导致每次开发老项目都很痛苦,光启动每次就要花费至少一分钟。

于是就想着使用最新的工具vite进行提速,不过在尝试了一番后,发现vite貌似不支持react15版本(jsx-runtime的破坏性改动,官方的vite-react插件暂未支持低版本reac),而且vite也不支持命名不为.module.xx的样式文件
看源码:
image.png
(这个问题,可以通过自己写一个插件解决,思路就是在rollup的transform阶段,ast语句分析importdecalation语句,手工替换文件后缀名解决),因为vite用户的插件是在vite构建插件之前运行的,所以可以生效。

代码示例:

  1. import { transformSync } from '@babel/core';
  2. const cssLangs = `\\.(css|less|sass|scss)($|\\?)`;
  3. const importRE = /import\s+([\S]+)\s+from\s+('|")([\S]+)\.(css|less|sass|scss)(\?[\S]*)?('|")/;
  4. const jsRE = /\.(js|ts|jsx|tsx)/;
  5. const cssLangRE = new RegExp(cssLangs);
  6. const cssModuleRE = new RegExp(`\\.module${cssLangs}`);
  7. function autoCSSModulePlugin() {
  8. return () => {
  9. return {
  10. visitor: {
  11. ImportDeclaration: (path) => {
  12. const { node } = path;
  13. if (!node) {
  14. return;
  15. }
  16. // 如果不是module css的那么就通过 转化为 module.styl来模块化css
  17. if (
  18. node.specifiers &&
  19. node.specifiers.length > 0 &&
  20. cssLangRE.test(node.source.value) &&
  21. !cssModuleRE.test(node.source.value)
  22. ) {
  23. const cssFile = node.source.value;
  24. node.source.value = cssFile + (cssFile.indexOf('?') > -1 ? '&' : '?') + '.module.less';
  25. }
  26. },
  27. },
  28. };
  29. };
  30. }
  31. function transform(code, { sourceMap, file }) {
  32. const parsePlugins = ['jsx'];
  33. if (/\.tsx?$/.test(file)) {
  34. parsePlugins.push('typescript');
  35. }
  36. const result = transformSync(code, {
  37. configFile: false,
  38. parserOpts: {
  39. sourceType: 'module',
  40. allowAwaitOutsideFunction: true,
  41. plugins: parsePlugins,
  42. },
  43. sourceMaps: true,
  44. sourceFileName: file,
  45. inputSourceMap: sourceMap,
  46. plugins: [autoCSSModulePlugin()],
  47. });
  48. return {
  49. code: result.code,
  50. map: result.map,
  51. };
  52. }
  53. export default function viteTransformCSSModulesPlugin() {
  54. const name = 'vite-plugin-transform-css-modules';
  55. return {
  56. name,
  57. transform(code, id) {
  58. if (jsRE.test(id) && importRE.test(code)) {
  59. const result = transform(code, {
  60. file: id,
  61. sourceMap: this.getCombinedSourcemap(),
  62. });
  63. if (result) {
  64. return result;
  65. }
  66. }
  67. return undefined;
  68. },
  69. };
  70. }

而且发起请求数也较多,尤其是首屏渲染时特别慢。不像webpack将多个模块合并成一个bundle,减少了请求。

image.png

这个原因一部分是因为我们的项目组件拆的很多,还有一个原因是用了antd的按需加载使用了less文件,在页面的加载的时候vite其实还是需要去编译less文件。

webpack 的打包模式在项目本身源码模块数量极大 (>1000) 的情况下还是有一点优势的,因为浏览器在处理这个级别的并发请求上会产生阻塞(但通常来说如果你一个路由下模块数到这个级别说明你代码分割/按需加载没做好)

二、预备知识

(一)esbuild

为什么我要提esbuild,因为vite使用了esbuild构建,而且webpack也可以使用esbuild-loader进行提速。

ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。

为什么这么快?

  1. js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
  2. go是纯机器码,肯定要比JIT快
  3. 不使用 AST,优化了构建流程(也带来了一些缺点,一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中,如果你的项目使用了 babel-plugin-import, 或者一些自定义的 babel-plugin ,无法使用)

四、webpack性能分析

1、webpack-bundle-analyzer

使用bundle-analyzer分析工具分析包的大小:
就拿其中路由比较少的一个项目admin-crm-web来启动,也有80多个路由:
image.png
image.png
image.png
image.png
启动时间1分半,占用内存6.77G,所有的chunk加起来有100MB

就算手动选择某个路由,仍需10几秒左右的时间。

2、speed-measure-webpack-plugin

使用speed-measure-webpack-plugin进行分析,测量各个插件和loader所花费的时间:
image.png
image.png

五、优化方案

4、浏览器驱动路由切割编译

image.png
之前的项目如果要启动的话,我们的插件falcon-webpack为了减少构建时候的打包路由,让我们进行了手动选择,这样做还是不够方便:
1、路由太多不方便选择
2、选择错了,需要关闭然后重启再次选择

想想vite是怎么做的,vite启动的时候,没有做任何的编译,等你访问页面的时候,通过浏览器的esm请求资源,vite然后根据你需要的资源进行按需编译,返回给浏览器。

为了改进使用体验,提示开发效率,我在想,能不能启动的时候不选择路由,当我浏览器访问页面的时候,再告诉webpack我需要打包这个页面,webpack然后再去打包当前页面呢?

视频地址:视频1

(备用地址 链接: https://pan.baidu.com/s/1OW7xT-RelfS3UkO_FmNu1g 提取码: 178r 复制这段内容后打开百度网盘手机App,操作更方便哦)

我的思路是,在webpack开发时候,打包空路由,然后在页面中注入脚本,监听路由变化,发送请求到webpack,通过webpack-dev-server的before钩子监听请求,获取路由变化的参数,然后在动态写入路由。
下面是我的实现:
image.pngimage.png

可以提升的点:
1、打包的时候页面一片空白,可以加入loading或者打包进度输出
2、需要一份.routes.ts文件,这份文件和你的的路由文件是相同的,只是过滤后的文件,可以用babel重写,在importDecleration中进行匹配,在内存中进行过滤

2、UglifyJsPlugin 开启parallel

利用多核处理器进行并行处理。原理就是使用nodejs启用了新的子进程。
不过后来我升级了webpack版本到5,使用了terser-plugin,也是一样的。

3、dll插件

dll和external的功能是一样的,不过是对本地的依赖进行预打包,然后再排除,如果依赖变化了,还需要再重新打包一次。
image.png
dll插件不仅可以生产环境用,构建时也能用,所以可以放开。所以我把已经生成好的dll文件放在了本地目录下,
image.png
并设置了
image.png
然后在页面引入
image.png

4、升级webpack

检查过期的包:
image.png
升级和webpack相关的包:
image.png
升级完以后再安装webpack-cli:
image.png

5、使用esbuild

不过esbuild-loader也有缺点:

(1)不支持装饰器

使用oneOf对单文件采用单一loader, 当returen false的时候即采用babel-loader + ts-loader形式打包文件;
image.png

判断输入文件是否有装饰器:

  1. /** 判断是否具有装饰器 */
  2. function hasDecorator(fileContent, offset = 0) {
  3. const atPosition = fileContent.indexOf('@', offset);
  4. if (atPosition === -1) {
  5. return false;
  6. }
  7. if (atPosition === 1) {
  8. return true;
  9. }
  10. if (["'", '"'].includes(fileContent.substr(atPosition - 1, 1))) {
  11. return hasDecorator(fileContent, atPosition + 1);
  12. }
  13. return true;
  14. }

(2)不支持按需加载

开发环境使用esbuild-loader,antd全量引入即可

(3)只能打包成es6

image.png

但是我们的项目一般都是打包成es5的,所以会存在冲突,所以可以在开发环境,设定ts-loader的configfile
image.png

8、exclude改为include

这样能将 loader 应用于最少数量的必要模块
image.png
改为
image.png

9、尽量少地使用工具

每个额外的 loader/plugin 都有其启动时间。
image.png
webpack - 图137
例如一些插件,在生产环境才需要,开发环境可以不使用例如:BannerPlugin、ExtractTextPlugin、UglifyJsPlugin等

10、Devtool

需要注意的是不同的 devtool 设置,会导致性能差异。

  • “eval” 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
  • 使用 eval-source-map 变体配置进行增量编译。

在大多数情况下,最佳选择是 eval-cheap-module-source-map

11、开发环境避免额外的优化步骤

Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:
image.png

12、开发环境输出结果不携带路径信息

Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:
image.png

13、开发环境ts-loader的优化

你可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。
image.png

14、happypack或者thread-loader

webpack3可以使用happypack,webpack5使用thread-loader,不过也是有一些限制的:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。
请仅在耗时的操作中使用此 loader!
webpack - 图141
可以看到耗时的几个loader,所以进行配置:
image.png
又遇到了坑:
image.png
主要原因就是项目中js文件使用了ts的语法,导致我不得不也用了ts-loader将js文件也进行了转换,这样的做法是不严谨的,但是老项目也没有办法。

15、noParse

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 webpack 会引入这些模块,但是不进行转化和解析,从而提升 webpack 的构建性能 ,例如:jquery 、lodash。
image.png

16、externals

已经使用了dll排除了一些第三方依赖,但是每个具体的项目还有一些外部依赖也可以排除,在不重现打包dll文件的情况下,可以使用externals。
image.png[

](https://segmentfault.com/a/1190000011795931)

17、优化 resolve.extensions 配置

在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:

  • resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
  • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。

image.png
移除.web.js

18、开启缓存

例如babel-loader开启缓存,第二次构建时会读取缓存:
image.png
webpack自带的cache不用配置,默认开发环境开启。

HtmlWebpackPlugin、TerserPlugin、eslint-loader、babel-loader统统加上缓存

除了这些之外,还有cache-loader、HardSourceWebpackPlugin都是可以开启缓存的。我这里没做尝试,大家可以有兴趣试一试。

19、webpack编译两次

image.png
在使用webpack3的时候,经常会出现编译2次的情况,好像是低版本的html-webpack-plugin才有的问题,在我升级完以后,这个问题已经不存在了。

[

](https://webpack.docschina.org/guides/build-performance/#incremental-builds)

六、踩坑

坑1:

image.png将dll的json中的meta替换为buildMeta即可。

坑2:

image.png
jsonpFunction 替换为chunkLoadingGlobal

坑3:

image.png
image.png
将loader替换为use即可。

坑4:

image.png
原因是因为falcon插件:
image.png
webpack的api变化,应该改写为
image.png

坑5:

image.png

image.png
改为:
image.png

坑6:

image.png
contentBase改为static

坑7:

image.png
disableHostCheck改为allowedHosts:’all’

坑8:

image.png
改为logging:’warn’

坑9:

image.png
webpack.optimize.CommonsChunkPlugin 升级到splitChunks
image.png

坑10:

image.png
将devserver的before换成onBeforeSetupMiddleware
image.png

坑11:

image.png
babel.rc文件中plugin不允许使用数组的配置了
image.png

坑12:

image.png
主要原因还是babel的预设都已经改为了@babel/preset-es2015这样的形式
image.png
image.png
改为安装新的预设包,并改写配置项:
image.png

坑13:

image.png
全局安装webpack-cli

坑14:

image.png
移除babel配置中的,并将@babel/preset-es2015和@babel/preset-stage-0换成@babel/preset-env
image.png

坑15:

image.png

将之前的写法改为
image.png
image.png

坑16:

image.png
改写为
image.png

坑17:

image.png
使用mini-css-extract-plugin代替extract-text-webpack-plugin即可。
image.pngimage.png
改写为:
image.pngimage.png

坑18:

image.png
对ossPlugin进行升级。

坑19:

image.png
安装 @babel/plugin-proposal-decorators插件即可。
image.png

坑20:

image.png
因为使用了babel-plugin-import,所以这个ts-import-plugin是多余的,可以去掉。
image.png

坑21:

image.png
image.png
这些报错主要是因为在js中使用了ts的功能,所以
image.png
js文件也应该用ts-loader

坑22:

image.png
类型断言只能在ts文件中使用

坑23:

image.png
zlib找不到,原因是webpack5以前是内置了nodejs的一些polyfills的,现在需要自己单独安装配置。
image.png

坑24:

image.png
还是因为装饰器的转换插件丢失了descriptor,
主要原因还是在我们的项目中,装饰器的使用的方法是
image.png
而官方的使用方法是:
image.png
在我们的项目中装饰的是class的propotery,而不是class method,所以最后根本没有走装饰器插件那里的转换。
image.png
装饰器代码注释了以后,页面正常,说明已经是最后一个坑了。image.png
这里只有2个解决办法,一个就是将项目中的代码改为和官方一样的写法,还有一个就是自己再把以前老的装饰器的插件对于class的propetry那块拿来重新一个自定义插件。

坑25:

image.png
uglifyjs-webpack-plugin改为terser-webpack-plugin
image.png

坑26:

image.png
安装配置assert
image.png

坑27:

image.png
安装配置stream-browserify
image.png

坑28:

image.png
加入ignoreOrder
image.png

坑29:
image.png
使用了esbuild之后,虽然速度提升了好几倍,
image.png
但是在webpack中配置的reslove的模块找不到引用了
image.png
image.png
image.png
因为这里项目中也是在babel中改变引用了路径的,要修复的话,必须要把crm-comps下面src的文件新建一个index.js暴露出来即可。

坑29:

image.png
image.png
主要是main设置的有问题,项目中是通过babel改变了引入的目录的,现在使用esbuild就会报错
image.png
改为:
image.png

七、优化效果

image.png
全部启动只花了不到10秒,还记得前面没有优化之前启动一个整个项目需要多久吗?1份10几秒,时间提升了90%!

这里
image.pngimage.pngimage.png
内存使用减少了1.7G

一、冷启动一个大型老项目要多久?

视频地址一

备用地址(链接: https://pan.baidu.com/s/16u0A9wBPq1cuKVUWXamxNA 提取码: juwa )

再来看一个更夸张的:
69ce51d7-e109-4655-8766-e0a4f6c12a7f.png

二、缘起

老项目由于使用的webpack版本较老,而且页面又较多,而且对于性能优化方面并没有深入地进行挖掘。导致每次开发老项目都很痛苦,光启动每次就要花费至少一分钟。

于是就想着使用最新的工具vite进行提速,不过在尝试了一番后,发现vite貌似不支持react15版本(jsx-runtime的破坏性改动,官方的vite-react插件暂未支持低版本reac),而且vite也不支持命名不为.module.xx的样式文件
看源码:
image.png
(这个问题,可以通过自己写一个插件解决,思路就是在rollup的transform阶段,ast语句分析importdecalation语句,手工替换文件后缀名解决),因为vite用户的插件是在vite构建插件之前运行的,所以可以生效。

代码示例:

  1. import { transformSync } from '@babel/core';
  2. const cssLangs = `\\.(css|less|sass|scss)($|\\?)`;
  3. const importRE = /import\s+([\S]+)\s+from\s+('|")([\S]+)\.(css|less|sass|scss)(\?[\S]*)?('|")/;
  4. const jsRE = /\.(js|ts|jsx|tsx)/;
  5. const cssLangRE = new RegExp(cssLangs);
  6. const cssModuleRE = new RegExp(`\\.module${cssLangs}`);
  7. function autoCSSModulePlugin() {
  8. return () => {
  9. return {
  10. visitor: {
  11. ImportDeclaration: (path) => {
  12. const { node } = path;
  13. if (!node) {
  14. return;
  15. }
  16. // 如果不是module css的那么就通过 转化为 module.styl来模块化css
  17. if (
  18. node.specifiers &&
  19. node.specifiers.length > 0 &&
  20. cssLangRE.test(node.source.value) &&
  21. !cssModuleRE.test(node.source.value)
  22. ) {
  23. const cssFile = node.source.value;
  24. node.source.value = cssFile + (cssFile.indexOf('?') > -1 ? '&' : '?') + '.module.less';
  25. }
  26. },
  27. },
  28. };
  29. };
  30. }
  31. function transform(code, { sourceMap, file }) {
  32. const parsePlugins = ['jsx'];
  33. if (/\.tsx?$/.test(file)) {
  34. parsePlugins.push('typescript');
  35. }
  36. const result = transformSync(code, {
  37. configFile: false,
  38. parserOpts: {
  39. sourceType: 'module',
  40. allowAwaitOutsideFunction: true,
  41. plugins: parsePlugins,
  42. },
  43. sourceMaps: true,
  44. sourceFileName: file,
  45. inputSourceMap: sourceMap,
  46. plugins: [autoCSSModulePlugin()],
  47. });
  48. return {
  49. code: result.code,
  50. map: result.map,
  51. };
  52. }
  53. export default function viteTransformCSSModulesPlugin() {
  54. const name = 'vite-plugin-transform-css-modules';
  55. return {
  56. name,
  57. transform(code, id) {
  58. if (jsRE.test(id) && importRE.test(code)) {
  59. const result = transform(code, {
  60. file: id,
  61. sourceMap: this.getCombinedSourcemap(),
  62. });
  63. if (result) {
  64. return result;
  65. }
  66. }
  67. return undefined;
  68. },
  69. };
  70. }

而且发起请求数也较多,尤其是首屏渲染时特别慢。不像webpack将多个模块合并成一个bundle,减少了请求。

image.png

这个原因一部分是因为我们的项目组件拆的很多,还有一个原因是用了antd的按需加载使用了less文件,在页面的加载的时候vite其实还是需要去编译less文件。

webpack 的打包模式在项目本身源码模块数量极大 (>1000) 的情况下还是有一点优势的,因为浏览器在处理这个级别的并发请求上会产生阻塞(但通常来说如果你一个路由下模块数到这个级别说明你代码分割/按需加载没做好)

二、预备知识

(一)esbuild

为什么我要提esbuild,因为vite使用了esbuild构建,而且webpack也可以使用esbuild-loader进行提速。

ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。

为什么这么快?

  1. js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
  2. go是纯机器码,肯定要比JIT快
  3. 不使用 AST,优化了构建流程(也带来了一些缺点,一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中,如果你的项目使用了 babel-plugin-import, 或者一些自定义的 babel-plugin ,无法使用)

四、webpack性能分析

1、webpack-bundle-analyzer

使用bundle-analyzer分析工具分析包的大小:
就拿其中路由比较少的一个项目admin-crm-web来启动,也有80多个路由:
image.png
image.png
image.png
image.png
启动时间1分半,占用内存6.77G,所有的chunk加起来有100MB

就算手动选择某个路由,仍需10几秒左右的时间。

2、speed-measure-webpack-plugin

使用speed-measure-webpack-plugin进行分析,测量各个插件和loader所花费的时间:
image.png
image.png

五、优化方案

4、浏览器驱动路由切割编译

image.png
之前的项目如果要启动的话,我们的插件falcon-webpack为了减少构建时候的打包路由,让我们进行了手动选择,这样做还是不够方便:
1、路由太多不方便选择
2、选择错了,需要关闭然后重启再次选择

想想vite是怎么做的,vite启动的时候,没有做任何的编译,等你访问页面的时候,通过浏览器的esm请求资源,vite然后根据你需要的资源进行按需编译,返回给浏览器。

为了改进使用体验,提示开发效率,我在想,能不能启动的时候不选择路由,当我浏览器访问页面的时候,再告诉webpack我需要打包这个页面,webpack然后再去打包当前页面呢?

视频地址:视频1

(备用地址 链接: https://pan.baidu.com/s/1OW7xT-RelfS3UkO_FmNu1g 提取码: 178r 复制这段内容后打开百度网盘手机App,操作更方便哦)

我的思路是,在webpack开发时候,打包空路由,然后在页面中注入脚本,监听路由变化,发送请求到webpack,通过webpack-dev-server的before钩子监听请求,获取路由变化的参数,然后在动态写入路由。
下面是我的实现:
image.pngimage.png

可以提升的点:
1、打包的时候页面一片空白,可以加入loading或者打包进度输出
2、需要一份.routes.ts文件,这份文件和你的的路由文件是相同的,只是过滤后的文件,可以用babel重写,在importDecleration中进行匹配,在内存中进行过滤

2、UglifyJsPlugin 开启parallel

利用多核处理器进行并行处理。原理就是使用nodejs启用了新的子进程。
不过后来我升级了webpack版本到5,使用了terser-plugin,也是一样的。

3、dll插件

dll和external的功能是一样的,不过是对本地的依赖进行预打包,然后再排除,如果依赖变化了,还需要再重新打包一次。
image.png
dll插件不仅可以生产环境用,构建时也能用,所以可以放开。所以我把已经生成好的dll文件放在了本地目录下,
image.png
并设置了
image.png
然后在页面引入
image.png

4、升级webpack

检查过期的包:
image.png
升级和webpack相关的包:
image.png
升级完以后再安装webpack-cli:
image.png

5、使用esbuild

不过esbuild-loader也有缺点:

(1)不支持装饰器

使用oneOf对单文件采用单一loader, 当returen false的时候即采用babel-loader + ts-loader形式打包文件;
image.png

判断输入文件是否有装饰器:

  1. /** 判断是否具有装饰器 */
  2. function hasDecorator(fileContent, offset = 0) {
  3. const atPosition = fileContent.indexOf('@', offset);
  4. if (atPosition === -1) {
  5. return false;
  6. }
  7. if (atPosition === 1) {
  8. return true;
  9. }
  10. if (["'", '"'].includes(fileContent.substr(atPosition - 1, 1))) {
  11. return hasDecorator(fileContent, atPosition + 1);
  12. }
  13. return true;
  14. }

(2)不支持按需加载

开发环境使用esbuild-loader,antd全量引入即可

(3)只能打包成es6

image.png

但是我们的项目一般都是打包成es5的,所以会存在冲突,所以可以在开发环境,设定ts-loader的configfile
image.png

8、exclude改为include

这样能将 loader 应用于最少数量的必要模块
image.png
改为
image.png

9、尽量少地使用工具

每个额外的 loader/plugin 都有其启动时间。
image.png
webpack - 图247
例如一些插件,在生产环境才需要,开发环境可以不使用例如:BannerPlugin、ExtractTextPlugin、UglifyJsPlugin等

10、Devtool

需要注意的是不同的 devtool 设置,会导致性能差异。

  • “eval” 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
  • 使用 eval-source-map 变体配置进行增量编译。

在大多数情况下,最佳选择是 eval-cheap-module-source-map

11、开发环境避免额外的优化步骤

Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:
image.png

12、开发环境输出结果不携带路径信息

Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:
image.png

13、开发环境ts-loader的优化

你可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。
image.png

14、happypack或者thread-loader

webpack3可以使用happypack,webpack5使用thread-loader,不过也是有一些限制的:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。
请仅在耗时的操作中使用此 loader!
webpack - 图251
可以看到耗时的几个loader,所以进行配置:
image.png
又遇到了坑:
image.png
主要原因就是项目中js文件使用了ts的语法,导致我不得不也用了ts-loader将js文件也进行了转换,这样的做法是不严谨的,但是老项目也没有办法。

15、noParse

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用 noParse 来标识这个模块,这样 webpack 会引入这些模块,但是不进行转化和解析,从而提升 webpack 的构建性能 ,例如:jquery 、lodash。
image.png

16、externals

已经使用了dll排除了一些第三方依赖,但是每个具体的项目还有一些外部依赖也可以排除,在不重现打包dll文件的情况下,可以使用externals。
image.png[

](https://segmentfault.com/a/1190000011795931)

17、优化 resolve.extensions 配置

在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:

  • resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
  • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。

image.png
移除.web.js

18、开启缓存

例如babel-loader开启缓存,第二次构建时会读取缓存:
image.png
webpack自带的cache不用配置,默认开发环境开启。

HtmlWebpackPlugin、TerserPlugin、eslint-loader、babel-loader统统加上缓存

除了这些之外,还有cache-loader、HardSourceWebpackPlugin都是可以开启缓存的。我这里没做尝试,大家可以有兴趣试一试。

19、webpack编译两次

image.png
在使用webpack3的时候,经常会出现编译2次的情况,好像是低版本的html-webpack-plugin才有的问题,在我升级完以后,这个问题已经不存在了。

[

](https://webpack.docschina.org/guides/build-performance/#incremental-builds)

六、踩坑

坑1:

image.png将dll的json中的meta替换为buildMeta即可。

坑2:

image.png
jsonpFunction 替换为chunkLoadingGlobal

坑3:

image.png
image.png
将loader替换为use即可。

坑4:

image.png
原因是因为falcon插件:
image.png
webpack的api变化,应该改写为
image.png

坑5:

image.png

image.png
改为:
image.png

坑6:

image.png
contentBase改为static

坑7:

image.png
disableHostCheck改为allowedHosts:’all’

坑8:

image.png
改为logging:’warn’

坑9:

image.png
webpack.optimize.CommonsChunkPlugin 升级到splitChunks
image.png

坑10:

image.png
将devserver的before换成onBeforeSetupMiddleware
image.png

坑11:

image.png
babel.rc文件中plugin不允许使用数组的配置了
image.png

坑12:

image.png
主要原因还是babel的预设都已经改为了@babel/preset-es2015这样的形式
image.png
image.png
改为安装新的预设包,并改写配置项:
image.png

坑13:

image.png
全局安装webpack-cli

坑14:

image.png
移除babel配置中的,并将@babel/preset-es2015和@babel/preset-stage-0换成@babel/preset-env
image.png

坑15:

image.png

将之前的写法改为
image.png
image.png

坑16:

image.png
改写为
image.png

坑17:

image.png
使用mini-css-extract-plugin代替extract-text-webpack-plugin即可。
image.pngimage.png
改写为:
image.pngimage.png

坑18:

image.png
对ossPlugin进行升级。

坑19:

image.png
安装 @babel/plugin-proposal-decorators插件即可。
image.png

坑20:

image.png
因为使用了babel-plugin-import,所以这个ts-import-plugin是多余的,可以去掉。
image.png

坑21:

image.png
image.png
这些报错主要是因为在js中使用了ts的功能,所以
image.png
js文件也应该用ts-loader

坑22:

image.png
类型断言只能在ts文件中使用

坑23:

image.png
zlib找不到,原因是webpack5以前是内置了nodejs的一些polyfills的,现在需要自己单独安装配置。
image.png

坑24:

image.png
还是因为装饰器的转换插件丢失了descriptor,
主要原因还是在我们的项目中,装饰器的使用的方法是
image.png
而官方的使用方法是:
image.png
在我们的项目中装饰的是class的propotery,而不是class method,所以最后根本没有走装饰器插件那里的转换。
image.png
装饰器代码注释了以后,页面正常,说明已经是最后一个坑了。image.png
这里只有2个解决办法,一个就是将项目中的代码改为和官方一样的写法,还有一个就是自己再把以前老的装饰器的插件对于class的propetry那块拿来重新一个自定义插件。

坑25:

image.png
uglifyjs-webpack-plugin改为terser-webpack-plugin
image.png

坑26:

image.png
安装配置assert
image.png

坑27:

image.png
安装配置stream-browserify
image.png

坑28:

image.png
加入ignoreOrder
image.png

坑29:
image.png
使用了esbuild之后,虽然速度提升了好几倍,
image.png
但是在webpack中配置的reslove的模块找不到引用了
image.png
image.png
image.png
因为这里项目中也是在babel中改变引用了路径的,要修复的话,必须要把crm-comps下面src的文件新建一个index.js暴露出来即可。

坑29:

image.png
image.png
主要是main设置的有问题,项目中是通过babel改变了引入的目录的,现在使用esbuild就会报错
image.png
改为:
image.png

七、优化效果

image.png
全部启动只花了不到10秒,还记得前面没有优化之前启动一个整个项目需要多久吗?1份10几秒,时间提升了90%!

这里
image.pngimage.pngimage.png
内存使用减少了1.7Gimage.png
内存使用减少了1.7G