使用Webpack时sass-loader构建时间慢

36

总结

当我们开始使用Webpack处理SASS文件时,我们发现在某些情况下构建时间变得非常慢。通过使用SpeedMeasurePlugin来测量构建不同部分的性能,似乎sass-loader是罪魁祸首......它可以轻松地花费10秒来构建(在我们进行一些修复之前,它曾经需要20秒),这比我们想要的要长。

我很好奇是否有其他优化构建Sass资源的策略,我没有涉及到。我已经尝试了相当多的方法(其中一些多次尝试),但仍然无法将构建时间降低到足够低的水平。目前,对于大型重建(例如为多个文件中使用的组件进行更改),构建可能需要10-12秒,如果可能的话,我希望将其缩短到更接近5秒。

已尝试的解决方案

我们尝试了许多不同的解决方案,一些有效,另一些没有太大帮助。

  • HardSourcePlugin - 这个插件效果不错。根据构建时的缓存情况,它能够将构建时间减少几秒钟。
  • 移除重复的导入(比如在多个地方都导入相同的“variables.sass”文件)- 这也可以将构建时间缩短几秒钟。
  • 将我们的SASS和 SCSS混合改为只使用SCSS - 我不确定为什么这样做会有帮助,但它似乎确实可以减少一点构建时间。也许因为所有文件都是相同的文件类型,所以编译更容易?(可能还有其他原因造成了这种结果,但它似乎总是有所帮助)。
  • 使用fast-sass-loader 替换sass-loader - 很多人推荐这样做,但当我使用它时,它似乎并没有改变构建时间。我不知道为什么…也许有配置问题。
  • 利用cache-loader - 这似乎也没有任何改进。
  • 对Sass禁用源映射 - 这似乎有很大的影响,将构建时间减半(从应用更改之后开始计算)。
  • 尝试使用includePaths导入从node_modules加载的SASS - 在一个我找到的git issue上建议这样做,因为sass-loader在处理他们称为“custom importers”的东西时存在问题。我的理解是,通过使用includePaths,SASS能够依赖提供的绝对路径,而不是使用一种低效的算法来解决到诸如node_modules等地方的路径。

根据简要统计,我们似乎有约16k行SASS代码分布在150个SASS文件中。有些文件有相当多的代码,而其他文件则较少,这些文件的简单平均每个文件大约有107行代码。

下面是正在使用的配置。应用程序是一个Rails应用程序,所以大部分Webpack配置都由Webpacker gem处理。

{
  "mode": "production",
  "output": {
    "filename": "js/[name].js",
    "chunkFilename": "js/[name].js",
    "hotUpdateChunkFilename": "js/[id]-[hash].hot-update.js",
    "path": "myApp/public/packs",
    "publicPath": "/packs/"
  },
  "resolve": {
    "extensions": [".mjs", ".js", ".sass", ".scss", ".css", ".module.sass", ".module.scss", ".module.css", ".png", ".svg", ".gif", ".jpeg", ".jpg"],
    "plugins": [{
      "topLevelLoader": {}
    }],
    "modules": ["myApp/app/assets/javascript", "myApp/app/assets/css", "node_modules"]
  },
  "resolveLoader": {
    "modules": ["node_modules"],
    "plugins": [{}]
  },
  "node": {
    "dgram": "empty",
    "fs": "empty",
    "net": "empty",
    "tls": "empty",
    "child_process": "empty"
  },
  "devtool": "source-map",
  "stats": "normal",
  "bail": true,
  "optimization": {
    "minimizer": [{
      "options": {
        "test": {},
        "extractComments": false,
        "sourceMap": true,
        "cache": true,
        "parallel": true,
        "terserOptions": {
          "output": {
            "ecma": 5,
            "comments": false,
            "ascii_only": true
          },
          "parse": {
            "ecma": 8
          },
          "compress": {
            "ecma": 5,
            "warnings": false,
            "comparisons": false
          },
          "mangle": {
            "safari10": true
          }
        }
      }
    }],
    "splitChunks": {
      "chunks": "all",
      "name": false
    },
    "runtimeChunk": true
  },
  "externals": {
    "moment": "moment"
  },
  "entry": {
    "entry1": "myApp/app/assets/javascript/packs/entry1.js",
    "entry2": "myApp/app/assets/javascript/packs/entry2.js",
    "entry3": "myApp/app/assets/javascript/packs/entry3.js",
    "entry4": "myApp/app/assets/javascript/packs/entry4.js",
    "entry5": "myApp/app/assets/javascript/packs/entry5.js",
    "entry6": "myApp/app/assets/javascript/packs/entry6.js",
    "entry7": "myApp/app/assets/javascript/packs/entry7.js",
    "entry8": "myApp/app/assets/javascript/packs/entry8.js",
    "landing": "myApp/app/assets/javascript/packs/landing.js",
    "entry9": "myApp/app/assets/javascript/packs/entry9.js",
    "entry10": "myApp/app/assets/javascript/packs/entry10.js",
    "entry11": "myApp/app/assets/javascript/packs/entry11.js",
    "entry12": "myApp/app/assets/javascript/packs/entry12.js",
    "entry13": "myApp/app/assets/javascript/packs/entry13.js",
    "entry14": "myApp/app/assets/javascript/packs/entry14.js",
    "entry15": "myApp/app/assets/javascript/packs/entry15.js"
  },
  "module": {
    "strictExportPresence": true,
    "rules": [{
      "parser": {
        "requireEnsure": false
      }
    }, {
      "test": {},
      "use": [{
        "loader": "file-loader",
        "options": {
          "context": "app/assets/javascript"
        }
      }]
    }, {
      "test": {},
      "use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
        "loader": "css-loader",
        "options": {
          "sourceMap": true,
          "importLoaders": 2,
          "localIdentName": "[name]__[local]___[hash:base64:5]",
          "modules": false
        }
      }, {
        "loader": "postcss-loader",
        "options": {
          "config": {
            "path": "myApp"
          },
          "sourceMap": true
        }
      }],
      "sideEffects": true,
      "exclude": {}
    }, {
      "test": {},
      "use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
        "loader": "css-loader",
        "options": {
          "sourceMap": true,
          "importLoaders": 2,
          "localIdentName": "[name]__[local]___[hash:base64:5]",
          "modules": false
        }
      }, {
        "loader": "postcss-loader",
        "options": {
          "config": {
            "path": "myApp"
          },
          "sourceMap": false,
          "plugins": [null, null]
        }
      }, {
        "loader": "sass-loader",
        "options": {
          "sourceMap": false,
          "sourceComments": true
        }
      }],
      "sideEffects": true,
      "exclude": {}
    }, {
      "test": {},
      "use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
        "loader": "css-loader",
        "options": {
          "sourceMap": true,
          "importLoaders": 2,
          "localIdentName": "[name]__[local]___[hash:base64:5]",
          "modules": true
        }
      }, {
        "loader": "postcss-loader",
        "options": {
          "config": {
            "path": "myApp"
          },
          "sourceMap": true
        }
      }],
      "sideEffects": false,
      "include": {}
    }, {
      "test": {},
      "use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
        "loader": "css-loader",
        "options": {
          "sourceMap": true,
          "importLoaders": 2,
          "localIdentName": "[name]__[local]___[hash:base64:5]",
          "modules": true
        }
      }, {
        "loader": "postcss-loader",
        "options": {
          "config": {
            "path": "myApp"
          },
          "sourceMap": true
        }
      }, {
        "loader": "sass-loader",
        "options": {
          "sourceMap": true
        }
      }],
      "sideEffects": false,
      "include": {}
    }, {
      "test": {},
      "include": {},
      "exclude": {},
      "use": [{
        "loader": "babel-loader",
        "options": {
          "babelrc": false,
          "presets": [
            ["@babel/preset-env", {
              "modules": false
            }]
          ],
          "cacheDirectory": "tmp/cache/webpacker/babel-loader-node-modules",
          "cacheCompression": true,
          "compact": false,
          "sourceMaps": false
        }
      }]
    }, {
      "test": {},
      "include": ["myApp/app/assets/javascript", "myApp/app/assets/css"],
      "exclude": {},
      "use": [{
        "loader": "babel-loader",
        "options": {
          "cacheDirectory": "tmp/cache/webpacker/babel-loader-node-modules",
          "cacheCompression": true,
          "compact": true
        }
      }]
    }, {
      "test": "myApp/node_modules/jquery/dist/jquery.js",
      "use": [{
        "loader": "expose-loader",
        "options": "jQuery"
      }, {
        "loader": "expose-loader",
        "options": "$"
      }]
    }, {
      "test": "myApp/node_modules/popper.js/dist/umd/popper.js",
      "use": [{
        "loader": "expose-loader",
        "options": "Popper"
      }]
    }, {
      "test": "myApp/node_modules/scroll-depth/jquery.scrolldepth.js",
      "use": [{
        "loader": "expose-loader",
        "options": "scrollDepth"
      }]
    }]
  },
  "plugins": [{
    "environment_variables_plugin": "values don't really matter in this case I think"
  }, {
    "options": {},
    "pathCache": {},
    "fsOperations": 0,
    "primed": false
  }, {
    "options": {
      "filename": "css/[name]-[contenthash:8].css",
      "chunkFilename": "css/[name]-[contenthash:8].chunk.css"
    }
  }, {}, {
    "options": {
      "test": {},
      "cache": true,
      "compressionOptions": {
        "level": 9
      },
      "filename": "[path].gz[query]",
      "threshold": 0,
      "minRatio": 0.8,
      "deleteOriginalAssets": false
    }
  }, {
    "pluginDescriptor": {
      "name": "OptimizeCssAssetsWebpackPlugin"
    },
    "options": {
      "assetProcessors": [{
        "phase": "compilation.optimize-chunk-assets",
        "regExp": {}
      }],
      "assetNameRegExp": {},
      "cssProcessorOptions": {},
      "cssProcessorPluginOptions": {}
    },
    "phaseAssetProcessors": {
      "compilation.optimize-chunk-assets": [{
        "phase": "compilation.optimize-chunk-assets",
        "regExp": {}
      }],
      "compilation.optimize-assets": [],
      "emit": []
    },
    "deleteAssetsMap": {}
  }, {
    "definitions": {
      "$": "jquery",
      "jQuery": "jquery",
      "jquery": "jquery",
      "window.$": "jquery",
      "window.jQuery": "jquery",
      "window.jquery": "jquery",
      "Popper": ["popper.js", "default"]
    }
  }, {
    "definitions": {
      "process.env": {
        "MY_DEFINED_ENV_VARS": "my defined env var values"
      }
    }
  }, {
    "options": {}
  }]
}


为什么所有的 "test": {}, 都是空的? - SuperDJ
好问题。我认为那些加载器是Rails的Webpacker gem自动生成的配置的一部分。 我怀疑这是因为这些加载器被添加了,但在开发中没有使用...但我对此并不确定。例如,在开发中未使用mini-css-extract-plugin来提取CSS。我的理解是,像那样有一个空测试只会不匹配任何文件,因此可能不会影响构建时间。 - mc92
空的 test 明显是由于 JSON.stringify - 尝试在对 Webpacker 环境配置进行字符串化之前定义 RegExp.prototype.toJSON = RegExp.prototype.toString;(用于调试)。 - Tim Krins
2
这可能与底层的sass包有关,而不是sass-loader本身。对于未来的读者,请检查您是否正在使用sass包或node-sass包。经过我的测试,我发现对于复杂项目,node-sass非常慢,而sass则快得多。 - Tim
不要使用Webpack,尝试切换到ESBuild。我在这里记录了一个简单的设置:https://dev59.com/71EG5IYBdhLWcg3wN25K#70325816 - Arslan Akram
显示剩余3条评论
1个回答

0
我认为有几个问题:
1:可能是因为您没有指定要测试哪些文件(所以webpack可能正在搜索一切):
...
"test": {},
  "use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
   "loader": "css-loader"
}
...

尝试将test: {}替换为test: /\.(sa|sc|c)ss$/或者test: /\.module.(sa|sc|c)ss$/(如果使用css模块)

1b:还可以尝试修改其他加载器中的test键,例如babel通常只需要查找js/ts(x)文件,因此如果是这种情况,请指定。

1c:也可以尝试调整include/exclude属性

2:在该配置中,您有4个css-loader实例 - 除非您正在进行服务器端渲染,否则您只需要两个(一个用于sc/sa/css模块,一个用于普通的sa/sc/ss)

这里是一个示例css loader配置,希望对您有所帮助(提示:css想要加载的方式通常因项目而异,因此请务必检查webpack/css-loader文档中的模块选项)

{
  test: /\.(sa|sc|c)ss$/,
  use: [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        esModule: true,
        modules: {
          namedExport: true,
        },
      },
    },
    {
      loader: 'css-loader',
      options: {
        sourceMap: !isProd,
        importLoaders: 2,
        esModule: true,
        modules: {
          auto: true,
          namedExport: true,
        },
      },
    },
    {
      loader: 'postcss-loader',
      options: {
        sourceMap: !isProd,
      },
    },
    {
      loader: 'sass-loader',
      options: {
        sourceMap: !isProd,
      },
    },
  ]
}

注意事项:webpack加载器是按照相反的顺序进行处理的,所以对于这个示例配置,处理的顺序是sass -> postcss -> css -> minicss。

不幸的是,对于使用webpack的CSS问题很少有直接明确的答案,需要仔细查阅文档并找出适合你项目的选项。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接