Webpack 3 - 打包從優化到放棄

背景

某一天,我忽然發現構建項目會常常失敗,直接報錯:FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory,這個錯誤很明顯就是內存不足致使的構建失敗。因爲項目是在CI / CD 上構建的,而在此期間運維又調整了一下資源上限,所以什麼緣由致使的還得進一步排查,是因爲真的內存不足仍是存在內存泄漏?css

內存問題

突破限制

在64位計算機上,V8引擎的默認內存限制爲約爲1.5GB,就算有再多的RAM也無濟於事,可是也不是沒有辦法,NodeJs容許咱們設置節點進程的內存,也就是經過參數max_old_space_size,咱們能夠暫時先設置內存限制,至少能先讓程序構建成功。html

// 增長上限至 4096 MB,該內存只要計算機支持就行。
node --max_old_space_size=4096 build.js
// 或者 
// 增長上限至 4194304 KB
node --max_new_space_size=4194304 build.js
複製代碼

調查問題

調整後發現再也沒有出現問題了,可是莫名其妙的爲啥會出現內存不足的問題呢?不過也能夠理解,項目愈來愈大,不免會出現內存不足,CPU 暴增的問題。如今咱們利用Chrome DevTools排查咱們的構建程序。node

排查的過程比較考驗耐心,由於電腦配置低跑起來都很慢。既然說到利用Chrome DevTools,那咱們就要製造證據。推薦使用 node-nightly 或者 node-heapdump 配合 memwatch-next。在這裏咱們使用 node-nightly。安裝以及使用方法連接上有,就很少說了。react

我採集了堆內存分配樣本堆內存動態分配時間線。結果發現並無異常的內存持續增加的狀況。雖說有少部分引用沒有回收,但不至於內存泄漏。有兩處忽然增加的緣由是一、實例化 Compiler,繼承 Tapable 插件框架,實現註冊和調用一系列插件;二、實例化插件,如 UglifyJsPlugin,而後讀取源文件,編譯並輸出,在這裏咱們還輸出了sourcemap (特殊緣由,須要輸出)。webpack

堆內存分配樣本git

堆內存動態分配時間線程序員

參考資料

新的問題 - 打包速度

內存問題解決了以後發如今本地打包速度也異常的慢(注:構建環境會影響打包速度,可是線上的構建環境資源是共享的,所以拿本地電腦來測試,構建時間因人而異)。目前的打包圖以下:es6

而同事(高端程序員)的電腦在未優化前則是這樣:github

話很少說,由於配置問題,纔會致使我有優化的慾望,低端配置以下:web

  • 型號:MacBook Air(13-inch, 2017);
  • 處理器:1.8 GHz Intel Core i5
  • 內存:8 GB 1600 MHz DDR3

打包相關以下:

  • 腳手架:create-react-app v1
  • 技術棧:React / Typescript / Antd / Less
  • 打包優化上還處理了 Code Splitting,ExtractText,UglifyJs 等。

工欲善其事,必先利其器

如今就開始選擇工具,來對咱們的項目進行分析。候選工具備progress-bar-webpack-plugin/webpackbar/speed-measure-webpack-plugin。咱們想要的效果,是最好能分析出哪個階段的耗時。所以咱們來比較一下這些工具是否匹配咱們的需求。PS:webpack —progress,並不知足咱們的需求,由於是信息太過於簡單讓咱們無處排查問題。

progress-bar-webpack-plugin

從下圖能夠看出 progress-bar-webpack-pluginwebpack --progress同樣不知足咱們的需求,它只是展現打包的進度信息。

webpackbar

webpackbar 在不作任何的配置的前提下,也比 progress-bar-webpack-plugin 好,至少能知道卡在哪一步,加載 node_modules 依賴的過程。

咱們經過設置 profile 來獲取更多的信息,固然展現信息只有loaders,而咱們每每也須要 plugins 的耗時,固然你也能夠經過自定義輸出信息,在這裏咱們就不展開討論,有興趣的小夥伴能夠自行嘗試。

// 經過配置 profile 展現詳細的信息
plugins: [
  new WebpackBar({
    profile: true,
    reporters: ['profile'],  // 注意這裏的配置很關鍵,不然沒信息
  })
]
複製代碼

speed-measure-webpack-plugin(推薦)

speed-measure-webpack-plugin 能夠經過很簡單的配置,就能夠獲取 plugins以及loaders 的耗時。

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const smpWrapperConfig = smp.wrap({
  // 將 webpack 的配置做爲參數傳給 SpeedMeasurePlugin
  ...webpackConfig,
});
module.exports = smpWrapperConfig;
複製代碼

測試

咱們用 speed-measure-webpack-plugin 來檢測下咱們每一個階段的耗時,可是值得注意的是,咱們只須要關注哪個階段的耗時最長,而不須要關注它跑了多長時間,由於 speed-measure-webpack-plugin 的加入也會拖慢咱們構建的時間。(這是我反覆測試的結果,假若有問題,麻煩請指出😂)

咱們使用 speed-measure-webpack-plugin 來測試一下,發現UglifyJsPlugin 佔時最長,調研了一下發現 github issue 上有很多這樣的問題,甚至出現了咱們上文出現的 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 的問題:

優化嘗試

在上文提到,項目構建速度慢,UglifyJsPlugin 佔一半。固然網上還有不少打包速度優化的手段,在這裏不作展開,一是由於效果不明顯,二是由於項目自己在早期也已經處理過,所以在這裏咱們針對性的優化一下。

webpack-parallel-uglify-plugin

我看網上有人推薦這個插件,可是其實在 CRA 中採用的 uglifyjs-webpack-plugin 也能夠經過參數 parallel: true 來達到多線程的做用,我測試過其實二者在速度上沒多大差異。更重要的是這個插件已經好久沒更新了,因此這個就直接跳過了,不推薦使用。

happypack

關於 happypack,我相信網上已經能找到不少關於它的傳聞,從單一進程構建模式到多進程模式,從而加速代碼構建,關於更多話很少說,有興趣的自行研究。happypack 支持的 loaders 能夠看這裏 Loader Compatibility,原理看這裏happypack 原理解析。部分配置以下:

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

// module
{
  test: /\.(ts|tsx)$/,
  include: resolveApp('src'),
  exclude: /node_modules/,
  use:'happypack/loader?id=tsx',
}
// less 的就不寫了。

// plugins
new HappyPack({
  id: 'tsx',
  threadPool: happyThreadPool,
  loaders: [
    {
      loader: require.resolve('ts-loader'),
      options: {
        happyPackMode: true,
        transpileOnly: true,
        getCustomTransformers: () => ({
          before: [
            tsImportPluginFactory([
              {
                libraryName: 'antd',
                libraryDirectory: 'es',
                style: true,
              },
            ]),
          ],
        }),
      },
    }
  ]
}),
複製代碼

不知道爲何,在個人電腦使用 happypack 以前的比使用 happypack 以後的首次速度還要快,可是緩存構建也是不分上下沒差多少。這是由於 happypack 對電腦的內核有必定的要求,假如電腦的內核低的狀況下又開啓多線程,反而會讓佔滿電腦的 CPU,總體速度變慢,所以這個方案也不是最好的選擇(反正個人電腦爛)。

terser-webpack-plugin

terser-webpack-plugin 是 webpack4 用來取代 uglifyjs-webpack-plugin 的壓縮插件,假如單純結合 webpack3 和 terser-webpack-plugin,不知道能不能解決壓縮速度的問題。

在 webpack3 中,官方提供的插件是 terser-webpack-plugin-legacy(看起來像是妥協版本)。從下圖能夠看出 ,oh my god(麻煩自行腦補李佳琦),這也太神奇了吧,簡直就是質的飛躍(不敢相信的我特地試了幾回)。

配置以下:

new TerserPlugin({
  parallel: true,
  cache: true,
  terserOptions: {
    parse: {
      ecma: 8,
    },
    compress: {
      ecma: 5,
      warnings: false,
      comparisons: false,
      inline: 2,
    },
  },
}),
複製代碼

小結

通過一段時間(具體不詳)的觀察,在 webpack 3 提高構建速度的方法有以下的方法:

  • 設置緩存,能夠有效的減小再次構建速度;
  • 使用 terser-webpack-plugin 替換 uglifyjs-webpack-plugin
  • 假如你能肯定某些模塊沒有依賴,能夠設置noParse
  • 使用 alias,這個能提高開發效率哦;
  • 使用 webpack-bundle-analyzer 剔除無關的依賴;
  • 在肯定模塊的狀況下,能夠配置 resolve.modules,如 resolve.modules = ['node_modules'],能夠減小搜索範圍;
  • loaders 可使用 test/include/exclude 來減小沒必要要的遍歷;
  • 在構建環境容許的狀況下,能夠試試 happypack

最後小彩蛋

假如你的項目使用了相似 React-Loadable進行按需加載,那麼請注意,React-Loadable 能夠幫助咱們根據路由來按需加載。它的原理是使用了import() 而非 import 是由於 import 是靜態編譯,而import()require,是能夠進行動態加載的。 可是千萬要注意的是,引用過程當中千萬不要使用變量,這會致使編譯經過可是編譯時間長得使人髮指又或者直接內存溢出。 - ES6 DYNAMIC IMPORT AND WEBPACK MEMORY LEAKS - Adrian Oprea - Medium

升級Webpack 4

那麼最後咱們來嘗試一下這個號稱編譯速度提高了 60% ~ 98% 的「黑科技」。因爲咱們是使用了create-react-app,所以咱們在升級過程當中會或多或少遇到不少問題,我在這裏記錄一下我升級過程當中遇到的問題。

因爲項目中已經 ejectcreate-react-app,所以不能使用官方推薦且快速的升級 react-scripts (本身挖的坑本身填)。

準備工做

yarn add -D webpack webpack-cli webpack-dev-server,升級webpack4 必備的三件套,缺一不可。別慌,準備工做其實就這麼多。慌的是如何處理升級後的兼容問題😂😂😂。

排除萬難

萬事開頭難,而後接着難,難上加難(滿臉寫着開心.jpg)。注意:每次解決問題就直接執行程序,即yarn start/build,下面就不贅述。

_this.compiler.applyPluginsAsync is not a function

👉🏻 升級 fork-ts-checker-webpack-plugin

Plugin could not be registered at 'html-webpack-plugin-before-html-processing'. Hook was not found. BREAKING CHANGE: There need to exist a hook at 'this.hooks'. To create a compatibility layer for this hook, hook into 'this._pluginCompat’.

👉🏻 升級 html-webpack-plugin@next 以及 react-dev-utils; 👉🏻 同時對配置文件(dev/prod)作如下優化:

// plugins
[
  new HtmlWebpackPlugin({
    ... // dev 和 prod 保持原來的配置
  }),
  new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw)
]
複製代碼

webpack is not a function

👉🏻 對 start.js 作如下優化:

// 調整爲對象結構
const compiler = createCompiler({ webpack, config, appName, urls, useYarn });
複製代碼

When specified, "proxy" in package.json must be a string. Instead, the type of "proxy" was "object". Either remove "proxy" from package.json, or make it a string.

👉🏻 安裝/升級 http-proxy-middleware; 👉🏻 將 package.json 中的 proxy 刪除,並添加src/setupProxy.js,並將其添加到paths.js; 👉🏻 修改 webpackDevServer.config.js

注意:

爲何要刪除 package.json 中的 proxy 呢?由於 proxypackage.json 中雖然以字符串存在,可是在默認狀況下仍是會優先讀取 package.json 中的 proxy 字段,其次纔是 setupProxy.js

// paths.js
module.exports = {
  ...,
  proxySetup: resolveApp('src/setupProxy.js'),
}

// webpackDevServer.config.js
before(app, server) {
  if (fs.existsSync(paths.proxySetup)) {
    require(paths.proxySetup)(app);
  }
}

// src/setupProxy.js
const proxy = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(proxy('/api', {
    target: 'https://xxx.xx.com',
    changeOrigin: true,
  }));
};
複製代碼

this.htmlWebpackPlugin.getHooks is not a function 假如報這個錯誤,那麼能夠嘗試如下操做:

👉🏻 刪除 node_modules 並從新安裝; 👉🏻 從新安裝 html-webpack-plugin@next; 👉🏻 確保 new InterpolateHtmlPlugin(env.raw) -> new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw)

DeprecationWarning: Pass resolveContext instead and use createInnerContext DeprecationWarning: Resolver: The callback argument was splitted into resolveContext and callback DeprecationWarning: Resolver#doResolve: The type arguments (string) is now a hook argument (Hook). Pass a reference to the hook instead.

這個不是錯誤,你能夠選擇忽略,也能夠作出如下處理: 👉🏻 升級 tsconfig-paths-webpack-plugin

Tapable.plugin is deprecated. Use new API on .hooks instead

👉🏻 升級 extract-text-webpack-plugin,可是在webpack4 已經不推薦使用該插件了,可使用 mini-css-extract-plugin 取代,值得注意的是使用 mini-css-extract-plugin 的同時能夠不使用style-loader ———Advanced configuration example

剩下的問題就是遇到什麼插件不兼容直接升級就能夠了,例如:

TypeError: Cannot read property 'ts' of undefined URIError: Failed to decode param ‘/%PUBLIC_URL%/favicon.ico’

👉🏻 升級ts-loader 以及 file-loader

extracting one single css file

如何使用 mini-css-extract-plugin 將全部的 css 文件都打包成一個css文件呢?其實有不少方法,咱們就使用官方推薦的方法Extracting all CSS in a single file,可是在這過程可能會報 Conflicting order between 的warnings,咱們能夠關閉警告 Remove Order Warnings。關於 CommonsChunkPlugin 能夠看這裏 RIP CommonsChunkPlugin.md · GitHub

// 關於更多 splitChunks 能夠查看 
// https://webpack.docschina.org/plugins/split-chunks-plugin/
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: { // entry 入口名稱
          name: 'styles', // 提取 chunk 的名稱
          test: /\.css$/,
          chunks: 'all', // initial | all | async,默認 async
          enforce: true,
        },
      },
    },
  },
  ...
}
複製代碼

替換依賴包

配置完畢以後,將下面的依賴包替換成 webpack4 推薦的依賴包。

  • extract-text-webpack-plugin -> mini-css-extract-plugin
  • uglifyjs-webpack-plugin -> terser-webpack-plugin

小結

到此 webpack4 基本上已經解決完畢了,剩下的問題,都是根據我的需求來處理了。升級到 webpack4 的過程不算太順利,可是這算是 webpack 的一個大版本,嘗試一下說不定就成功,畢竟 webpack4 進行了多處優化,一些存在安全問題的依賴包也獲得解決了,最後上一張升級後我本地和我同事構建的時間。

個人電腦

別人家的電腦

卒~
相關文章
相關標籤/搜索