Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

06. 排查一个webpack构建耗时异常的问题 #6

Open
xlkang opened this issue Jul 15, 2021 · 0 comments
Open

06. 排查一个webpack构建耗时异常的问题 #6

xlkang opened this issue Jul 15, 2021 · 0 comments

Comments

@xlkang
Copy link
Owner

xlkang commented Jul 15, 2021

最近做了一个商城项目,是在create react app创建的模版项目上跑eject命令把内部配置抛出来后魔改的,采用了Mpa+Spa的模式,拆分了多个html入口js入口

到了要上线的时候,发现在本地生产模式构建竟然需要2个小时,线上容器里跑也要三四十分钟,远远超出了正常的构建时间,影响了项目正常发布。

初步分析

综合了一下异常的现象,大概有以下几点:

  1. 构建耗时和构建页面的数量基本成正比(项目是多页的)
  2. 开发模式下构建耗时正常,生产模式下异常
  3. 只是耗时久,但是并没有报错

既然是打包的问题,那自然得从webpack身上找原因了。

结合第1个现象,同时为了方便在本地打包复现,整个过程中我只把打包其中3个页面的过程作为异常样板。

webpack打包大致有这几个阶段:依赖分析代码转换代码优化和压缩

初步分析配置文件,再结合第2点基本上可以合理推断,问题出在优化和压缩阶段(因为我们的项目中开发和生产模式构建在前两个阶段干的事情基本上差别不大)

为了佐证以上推测,先用webpack-bundle-analyzer跑了一遍,生产模式下构建产物的体积和依赖关系看起来没有很明显的问题。

定位排查

先从代码优化和压缩阶段入手,webpack4这部分基本上都走的optimization配置,这是这部分的配置代码:

optimization: {
  minimize: isEnvProduction,
  minimizer: [
    new TerserPlugin({
      terserOptions: {
        parse: {
          ecma: 8,
        },
        compress: {
          ecma: 5,
          warnings: false,
          comparisons: false,
          inline: 2,
        },
        mangle: {
          safari10: true,
        },
        keep_classnames: isEnvProductionProfile,
        keep_fnames: isEnvProductionProfile,
        output: {
          ecma: 5,
          comments: false,
          ascii_only: true,
        },
      },
      sourceMap: shouldUseSourceMap,
    }),
    new OptimizeCSSAssetsPlugin({
      cssProcessorOptions: {
        parser: safePostCssParser,
        map: shouldUseSourceMap
          ? {
              inline: false,
              annotation: true,
            }
          : false,
      },
      cssProcessorPluginOptions: {
        preset: ["default", { minifyFontValues: { removeQuotes: false } }],
      },
    }),
  ],
  splitChunks: {
    chunks: "all",
    name: false,
  },
  runtimeChunk: true,
},

重点关注optimization的几个一级配置项。

minimize是生产模式开启压缩的选项,minimizer是注册具体进行优化和压缩工作的插件及其具体配置,splitChunks是配置代码拆分规则,runtimeChunk是是否单独拆分运行时chunk的选项。

挨个来看,minimize不用管。

minimizer这里配置了TerserPluginOptimizeCSSAssetsPlugin两个插件,分别处理jscss,都有嫌疑,先待定。

splitChunks这里配置的是默认的自动拆分规则,webpack会自动把公共的依赖拆分到单独的bundle,其实从之前webpack-bundle-analyzer的分析结果比较容易看出拆分的包没有什么问题,同时也可以看到runtimeChunk也正常被拆分出来了, 基本上排除嫌疑了。

回到minimizer,想不如干,直接拿着这两个插件(只开启其中一个拆件)各跑了一遍build。在禁用OptimizeCSSAssetsPlugin的时候,构建耗时正常了!

好了,现在问题的范围缩小了,是OptimizeCSSAssetsPlugin

再来贴一遍这个插件的配置:

new OptimizeCSSAssetsPlugin({
  cssProcessorOptions: {
    parser: safePostCssParser,
    map: shouldUseSourceMap
      ? {
          inline: false,
          annotation: true,
        }
      : false,
  },
  cssProcessorPluginOptions: {
    preset: ["default", { minifyFontValues: { removeQuotes: false } }],
  },
})

先不管它是如何造成问题的,先看看这个插件做了什么事情。

也就两件事:优化css和压缩css。OptimizeCSSAssetsPlugin通过processor(默认是cssnano,基于postcss)优化css代码,再进行压缩。

到这里发挥一下“想象力”,OptimizeCSSAssetsPlugin是处理的css的,我们项目是使用scss的,也就是说构建到这一步的 时候scss到css已经转换完毕了,我们可以确定是转换后得到的css产物出了问题,这个锅OptimizeCSSAssetsPlugin不背啊,我只是进一步处理这些问题产物的!

接下来对css构建产物进行检查,对比了正常和异常的css构建产物,文件大小基本是差不多的,内容基本也是差不多的。但是为何耗时却差距几十倍上百倍?

我意识到OptimizeCSSAssetsPlugin干的一个很重要的事情被我忽略了,那就是去除重复代码!

再发挥一下“想象力”,异常的css产物中有大量重复代码,它们增加了OptimizeCSSAssetsPlugin的负担,但是被OptimizeCSSAssetsPlugin去除后看起来却很正常的一样,这样就说得通了。大量的重复代码,这是不正常的!

再次检查了css构建产物,这些代码引起了我的注意:

.m-1{margin:1px!important}
.pt-1{padding-top:1px!important}
.pl-1{padding-left:1px!important}
.pb-1{padding-bottom:1px!important}
.pr-1{padding-right:1px!important}
.p-1{padding:1px!important}
.mt-2{margin-top:2px!important}
.ml-2{margin-left:2px!important}
.mb-2{margin-bottom:2px!important}
/* 此处脑补... */

有经验的同学应该不陌生,这是一些Atom CSS,很多项目中都会使用到,一般在入口文件中统一引入作为全局样式使用,在我们的项目中是写在一个叫margin-padding.scss的文件中。

全局搜索这个文件名, 在webpack配置文件中发现了这些代码:

{
  loader: "sass-resources-loader",
  options: {
    resources: [
      path.resolve(__dirname, "../src/styles/variables.scss"),
      path.resolve(__dirname, "../src/styles/customTheme.scss"),
      path.resolve(
        __dirname,
        "../src/styles/margin-padding.scss"
      ),
    ],
  },
}

好家伙,真凶有点呼之欲出了。

记(git)忆(history)告诉我这是之前某同学为了在scss文件中自动注入变量而使用的一个loader。

variables.scss,customTheme.scss是一些项目中使用到的顶层scss变量,转换后其实不会产生任何css代码,没什么问题。

这个margin-padding.scss问题就有点大了,贴一段它的完整代码:

@for $member from 0 through 200 {
  .mt-#{$member} {
    margin-top: #{$member}px !important;
  }
  .ml-#{$member} {
    margin-left: #{$member}px !important;
  }
  .mb-#{$member} {
    margin-bottom: #{$member}px !important;
  }
  .mr-#{$member} {
    margin-right: #{$member}px !important;
  }
  .m-#{$member} {
    margin: #{$member}px !important;
  }
  .pt-#{$member} {
    padding-top: #{$member}px !important;
  }
  .pl-#{$member} {
    padding-left: #{$member}px !important;
  }
  .pb-#{$member} {
    padding-bottom: #{$member}px !important;
  }
  .pr-#{$member} {
    padding-right: #{$member}px !important;
  }
  .p-#{$member} {
    padding: #{$member}px !important;
  }
}

结合这个sass-resources-loader来看, 好家伙真的好家伙,原来我们给每个scss文件注入了2000条样式,而且它们全部被转换成了真正的css,并且经过合并,最后进入了要交到OptimizeCSSAssetsPlugin手中处理的css文件中

其实查阅sass-resources-loader的文档,文档中已经给出了警告:

Do not include anything that will be actually rendered in CSS, because it will be added to every imported Sass file.

不要包含任何实际将在CSS中呈现的内容,因为它将被添加到每个导入的Sass文件中!

到这里,基本宣告破案了。

解决

事实上,我们已经全局引入了margin-padding.scss,并且它是可以正常工作的,再通过sass-resources-loader去注入完全是多余并且错误的,所以去掉这一句就好。

去掉以后再运行全项目打包进行验证,1分钟跑完了,喜闻乐见。

后续

后来偶然间发现了speed-measure-webpack-plugin这个神器,它可以测量webpack构建期间各个阶段花费的时间,直接就可以找出瓶颈在哪,重点突破了。

我用这个工具重新跑了一遍之前的异常构建,工具很直观地显示出OptimizeCssAssetsWebpackPlugin花费了大量的时间。

回过头来看,如果一开始就使用speed-measure-webpack-plugin分析,可以省去手动定位到OptimizeCssAssetsWebpackPlugin之前所花费的时间。

在cra项目中的使用也很简单:

// /scripts/build.js
const configFactory = require("../config/webpack.config");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

// Generate configuration
const config = configFactory("production");

// const compiler = webpack(config); 
const compiler = webpack(smp.wrap(config));

这个工具不仅可以用来排查异常,也可以对分析和优化webpack构建速度提供很好的帮助。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant