5-webpack構建速度和體積優化策略


初級分析:使用webpack內置的stats

利用webpack內置的stats對象

它可以幫我們分析基本的一些信息,比如構建總共的時間,構建資源的大小

package.json 中使用 stats

指定輸出的是一個json對象,生成一個json文件

"scripts": {
  "build:stats": "webpack --config webpack.prod.js --json > stats.json"
}

node.js中使用

const webpack = require('webpack')
const config = require('./webpack.config.js')('production')

webpack(config, (err, stats) => {
  if (err) {
    return console.error(err)
  }
  if (stats.hasErrors()) {
    return console.error(stats.toString('errors-only'))
  }
  console.log(stats)
})

這兩種方式顆粒度太粗,看不出問題所在。想要分析實際的問題,比如哪個組件比較大,哪個loader耗的時間比較長,是無法很好的分析出來的。

速度分析:使用speed-measure-webpack-plugin

更好的分析webpack構建的速度,怎么找出構建速度問題所在。

使用speed-measure-webpack-plugin

可以看到每個loader和插件執行耗時,重點的關注耗時較長的loader或插件,針對這些做優化。

const SpeedMeatureWebpackPlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeatureWebpackPlugin()

const webpackConfig = smp.wrap({
  plugins: [
    new MyPlugin(),
    new MyOtherPlugin()
  ]
})

速度分析插件作用

分析整個打包總耗時

每個loader和插件的耗時情況

體積分析:使用webpack-bundle-analyzer

它可以把我們的項目打包出來的文件會進行一個分析,能很方便的看出體積的大小。面積越大體積越大,我們可以重點關注這些進行優化。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  
 module.exports = {
   plugins: [
     new BundleAnalyzerPlugin()
   ]
 }

構建完成后會在8888端口展示體積大小。

可以分析哪些問題

依賴的第三方模塊文件大小。

業務的組件代碼圖片大小,針對大的js可以做js的按需加載等優化操作。

使用高版本的webpack和Node.js

在 webpack 里做速度的優化。

在軟件這一塊,性能往往不是最大的問題,軟件不斷的迭代過程中,可以不斷的提升性能,對於構建而言同樣是適用的,所以推薦采用高版本的 webpack 和 node.js。

使用webpack4:優化原因

  • V8帶來的優化,很多對原生方法的優化(for of 代替 forEach、Map 和Set 代替 Object、includes 代替 indexOf)

  • 默認使用更快的 md4 的 hash 算法

  • webpacks AST 可以直接從 loader 傳遞給 AST,減少解析時間

  • 使用字符串的方法替代正則表達式

采用更高版本的node.js

高版本的node.js對原生的js API或數據結構是有做一些優化的。

驗證高版本node.js比低版本node.js性能更快,針對相同的api、相同的代碼做比較。

// 設置10000個key,運行100次

const runCount = 100
const keyCount = 10000

let map = new Map()

let keys = new Array(keyCount)
for (let i = 0; i < keyCount; i++) keys[i] = {}

for (let key of keys) map.set(key, true)

let startTime = process.hrtime()

for (let i = 0; i < runCount; i++) {
  for (let key of keys) {
    let value = map.get(key)
    if (value !== true) throw new Error()
  }
}

let elapsed = process.hrtime(startTime)
let [seconds, nanoseconds] = elapsed
console.log(elapsed)

let milliseconds = Math.round(seconds * 1e3 + nanoseconds * 1e-6)

console.log(`${process.version} ${milliseconds} ms`)

includes和indexOf的性能差異

const ARR_SIZE = 1000000
const hugeArr = new Array(ARR_SIZE).fill(1)

// includes
const includesTest = () => {
  const arrCopy = []
  console.time('includes')
  let i = 0
  while (i < hugeArr.length) {
    arrCopy.includes(i++)
  }
  console.timeEnd('includes')
}

// indexOf
const indexOfTest = () => {
  const arrCopy = []
  console.time('indexOf')
  for (let item of hugeArr) {
    arrCopy.indexOf(item)
  }
  console.timeEnd('indexOf')
}

includesTest()
indexOfTest()

多進程/多實例構建

多進程/多實例構建:資源並行解析可選方案

多進程/多實例:使用HappyPack解析資源

原理:每次 webapck 解析一個模塊,HappyPack 會將它及它的依賴分配給 worker 線程中。

每次 webpack 解析一個模塊,webpack 自身開啟一個進程去解析這個模塊。HappyPack 會將這個模塊進行划分,比如有多個模塊,在 webpack compiler run 方法之后,到達 HappyPack,它會做一些初始化,創建一個線程池,線程池會將構建任務里的模塊進行分配 ,比如將某個模塊以及它的依賴分配給 HappyPack 其中的一個線程,以此類推,那么一個 HappyPack 的線程池可能會包括多個線程,這些線程會各自的處理這些模塊以及它的依賴。處理完成之后,會有一個通信的過程,將處理好的資源傳輸給 HappyPack 的主進程,完成整個構建的過程。

module.exports = {
	module: {
		rules: [
			{
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          // 'babel-loader', 
          'happypack/loader' 
        ]
      }
		]
	},
	plugins: [
    new HappyPack({
      id: 'jsx',
      threads: 4,
      loaders: ['babel-loader?cacheDirectory=true']
    }),
    new HappyPack({
      id: 'styles',
      threads: 2,
      loaders: ['style-loader', 'css-loader', 'less-loader']
    })
  ]
}

多進程/多實例:使用thread-loader解析資源

webpack4 原生提供 thread-loader 這個模塊,它可以很好的替換 HappyPack,來做多進程/多實例的工作。

原理:跟 HappyPack 是差不多的。每次 webpack 解析一個模塊,thread-loader 會將它及它的依賴分配給worker 線程中。

在其他的loader之前放上thread-loader,做一系列的解析,最后會通過thread-loader進行處理。

module.exports = {
	module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: 3
            }
          },
          'babel-loader'
        ]
      }
    ]
  }
}

多進程/多實例並行壓縮代碼

方法一:使用 webpack-parallel-uglify-plugin 插件

const ParallelUglifyPluging = require('webpack-parallel-uglify-plugin')

module.exports = {
	plugins: [
    new ParallelUglifyPluging({
      uglifyJS: {
        output: {
          beautify: false,
          comments: false
        },
        compress: {
          warning: false,
          drop_console: true,
          collapse_vars: true,
          reduce_vars: true
        }
      }
    })
  ]
}

方法二:uglifyjs-webpack-plugin 開啟 parallel 參數

webpack3 推薦采用的插件,不支持 es6 代碼的壓縮。

const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
	plugins: [
    new UglifyjsWebpackPlugin({
      uglifyOptions: {
        warnings: false,
        parse: {},
        compress: {},
        mangle: true,
        output: null,
        tiplevel: false,
        nameCache: null,
        ie8: false,
        keep_fnames: false
      },
      parallel: true
    })
  ]
}

方法三:terser-webpack-plugin 開啟 parallel 參數

webpack4 默認使用的,支持es6代碼的壓縮。

const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true
      })
    ]
  }
}

進一步分包:預編譯資源模塊

分包:設置Externals

思路:將react, react-dom基礎包通過cdn引入,不打入bundle中。

方法:使用html-webpack-externals-plugin。

缺點:一個基礎庫需要指定一個cdn,實際的項目中有很多包,需要引入的script標簽太多 。

通過split-chunks-plugin插件分離基礎包,

缺點:它每次還是會對基礎包進行分析。

進一步分包:預編譯資源模塊

分包來說,更好的方式。

思路:將react、react-dom、redux、react-redux基礎包和業務基礎包打包成一個文件。

方法:使用 DLLPlugin 進行分包,DllReferencePlugin 對 manifest.json 引用。manifest.json 是對分離出來的包的描述。

使用 DLLPlugin 進行分包

創建一個單獨的構建配置文件,一般命名為 webpack.ddl.js,DLLPlugin 也會提高打包的速度。

const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: {
    library: [
      'react',
      'react-dom'
    ]
  },
  output: {
    path: path.join(__dirname, 'build/library'),
    filename: '[name].[chunkhash].dll.js',
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name].[hash]',
      path: path.join(__dirname, 'build/library/[name].json'),
    })
  ]
}

使用 DLLReferencePlugin 引用 manifest.json

在 webpack.config.js 中引入

module.exports = {
  plugins: [
    new webpack.DLLReferencePlugin({
      manifest: require('./build/library/manifest.json')
    })
  ]
}

充分利用緩存提升二次構建速度

緩存目的:提升二次構建速度。

緩存思路:

  • babel-loader 開啟緩存

  • terser-webpack-plugin 開啟緩存

    const TerserPlugin = require('terser-webpack-plugin')
    
    module.exports = {
      optimization: {
        minimize: true,
        minimizer: [
          new TerserPlugin({
            cache: true
          })
        ]
      }
    }
    
  • 使用 cache-loader 或者 hard-source-webpack-plugin

    • 針對模塊的緩存的開啟

      const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
      
      module.exports = {
        plugins: [
          new HardSourceWebpackPlugin()
        ]
      }
      

有緩存的話node_modules下面會有一個cache目錄

縮小構建目標

縮小構建目標

目的:盡可能的少構建模塊。

比如 babel-loader 不解析 node_modules。

module.exports = {
	module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: 'node_modules'
      }
    ]
  }
}

減少文件搜索范圍

優化 resolve.modules 配置(減少模塊搜索層級)

resolve.modules 是模塊解析的過程,webpack 解析時,模塊的查找過程和 nodejs 的模塊查找是比較類似的,會從當前的項目找,沒找到會去找 node_modules。會依次去子目錄找模塊是否存在。

優化 resolve.mainFields 配置

找入口文件的時候,會根據 package.json 里的 main 字段查找,因為發布到 npm 的組件的 package.json 會遵守一定的規范,都會有 main 這個字段,可以設置查找的時候直接讀取 main 這個字段,這樣也會減少一些不必要的分析過程。比如 package.json 里面沒有這個 main ,那它再去讀取根項目下的 index.js,沒有再去找 lib 下面的 index.js,這就是它默認的查找過程,我們把這個默認的查找過程鏈路做一個優化,只找 package.json 中 main 字段指定的入口文件。

優化 resolve.extensions 配置

模塊路徑的查找,比如 import 一個文件,沒有寫后綴,webpack 會先去找 .js,沒有會找 .json,默認情況下webpack 只支持 js 和 json 的讀取。extensions 數組里可以再設置其他的文件,如 .jsx .vue .ts 等。不過這個數組里面的內容越多的話,查找消耗的時間也會越多,因此我們可以縮小 extensions 查找的范圍,比如只設置查找 .js,其他文件需要寫的時候寫全文件后綴。避免 webpack 做不必要的查找。

合理使用 alias

別名,簡短的縮寫。比如模塊的路徑,我們找 react,它可能找了一圈,最后肯定是會找到 node_modules 里面去,它會經歷一系列的查找過程,我們可以把這一系列的過程直接給它寫好,告訴它比如你遇到了 react,就直接從指定的這個路徑去找。這個也大大的縮短了查找的時間。

module.exports = {
    // 子模塊的查找策略
    resolve: {
      alias: {
        'react': path.resolve(__dirname, './node_modules/react/umd/react.production.min.js'),
        'react-dom': path.resolve(__dirname, './node_modules/react-dom/umd/react-dom.production.min.js')
      },
      modules: [path.resolve(__dirname, 'node_modules')],
      extensions: ['.js'],
      mainFields: ['main']
    }
  }

使用Tree Shaking擦除無用的JavaScript和CSS

無用的css如何刪除掉?

PurifyCSS: 遍歷代碼,識別已經用到的 CSS class。

uncss: HTML 需要通過 jsdom 加載,所有的樣式通過 PostCSS 解析,通過 document.querySelector 來識別在 html 文件里面不存在的選擇器。

在 webpack 中如何使用 PurifyCSS?

使用 purgecss-webpack-plugin,它不能獨立使用,需要提取 css 為一個文件后才能使用。在 webpack4里需要和 mini-css-extract-plugin 配合使用,在 webpack3 里需要和 extract-text-webpack-plugin 配合使用。

const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgecssWebpackPlugin = require('purgecss-webpack-plugin')

const PATHS = {
  src: path.join(__dirname, 'src')
}

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css'
    }),
    new PurgecssWebpackPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true })
    })
  ]
}

使用webpack進行圖片壓縮

圖片資源相對是較大的,我們可以通過在線工具手動進行圖片的批量壓縮。構建工具一部分的職責就是將平時我們手動完成的事做成自動化。

圖片壓縮

要求:基於 Node 庫的 imagemin 或者 tinypng API。

使用:配置 image-webpack-loader。

module.exports = {
	module: {
    rules: [
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[hash:8].[ext]'
            }
          },
          {
            loader: 'image-webpack-loader',
            options: {
              // bypassOnDebug: true, // webpack@1.x
              // disable: true, // webpack@2.x and newer
              mozjpeg: {
                progressive: true,
                quality: 65
              },
              // optipng.enabled: false will disable optipng
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: [0.65, 0.90],
                speed: 4
              },
              gifsicle: {
                interlaced: false,
              },
              // the webp option will enable WEBP
              webp: {
                quality: 75
              }
            }
          }
        ]
      }
    ]
  }
}

Imagemin 的優點分析

  • 有很多定制選項

  • 可以引入更多第三方優化插件,例如pngquant

  • 可以處理多種圖片格式

Imagemin 的壓縮原理

  • pngquant: 是一款 PNG 壓縮器,通過將圖像轉換為具有 alpha 通道(通常比 24/32 位 PNG 文件小 60-80%)的更高效的 8 位 PNG 格式,可顯著減小文件大小。

  • pngcrush: 其主要目的是通過嘗試不同的壓縮級別和 PNG 過濾方法來降低 PNG IDAT 數據流的大小。

  • optipng: 其設計靈感來自於 pngcrush。optipng 可將圖像文件重新壓縮為更小尺寸,而不會丟失任何信息。

  • tinypng: 也是將 24 位 png 文件轉化為更小有索引的 8 位圖片,同時所有非必要的 metadata 也會被剝離掉。

使用動態Polyfill服務

Babel 默認只轉換新的 JavaScript 句法(syntax),而不轉換新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局對象,以及一些定義在全局對象上的方法(比如Object.assign )都不會轉碼。比如 ES6 在 Array 對象上新增了 Array.from 方法。Babel 就不會轉碼這個方法。如果想讓這個方法運行,必須使用 babel-polyfill。

構建體積優化:動態polyfill

babel-polyfill

打包后體積:88.49k,占比 29.6%

Promise 的瀏覽器支持情況

Polyfill 方案

Polyfill Service原理

識別 User Agent,下發不同的 Polyfill。

如何使用動態 Polyfill service

polyfill.io 官方提供的服務

https://polyfill.io/v3/polyfill.min.js

基於官方自建polyfill服務

//huayang.qq.com/polyfill/v3/polyfill.min.js?unknown=polyfill&features=Promise,Map,Set

體積優化策略總結:

  • Scope Hoisting

  • Tree Shaking

  • 公共資源分離

  • 圖片壓縮

  • 動態 polyfill


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM