webpack優化方案 | 實踐總結

本文 demo 地址

本文項目代碼位置: 源碼地址javascript

爲何須要構建工具

webpack 能幹什麼?css

  • 開發時,啓動本地服務
  • 解決 js、css 的依賴問題。(之前常常由於引入順序問題,致使 css 沒起做用或某個js變量找不到)
  • 將 ES六、vue/react、jsx 語法編譯爲瀏覽器可識別的代碼
  • 合併、壓縮、優化打包後的體積
  • css 前綴補齊/預處理器
  • 使用 eslint 校驗代碼
  • 單元測試

webpack 配置組成部分

module.exports = {
  entry: '',   // 指定入口文件
  output: '',  // 指定輸出目錄和輸出文件名
  mode: '',    // 環境
  module: {
    rules: [   // loader配置
      {test: '', use: ''}
    ]
  },
  plugins: [   // 插件配置
    new xxxPlugin()
  ]
}
複製代碼

基礎經常使用的 Loader

爲何須要 loader ?html

webpack 原生只支持 js、json 兩種模塊類型,因此須要 loader 把其餘類型的文件轉化成有效的模塊,並能夠添加到依賴圖中。vue

loader 自己是一個函數,接受源文件做爲參數,返回轉換的結果html5

功能 loader 說明
解析es6 babel-loader 配合.babelrc使用
解析vue vue-loader
解析css css-loader 用於加載.css文件,並轉換成commonjs對象
style-loader 將樣式經過<style>標籤插入到head中
解析less less-loader 將less轉換成css
解析圖片和字體 file-loader 用於處理文件(圖片、字體)
url-loader 也能夠處理圖片和字體,和file-loader功能相似,但它還能夠設置較小資源自動轉base64(內部用了file-loader),使用options:{limit: xxx}

基礎經常使用的 Plugin

插件用於 bundle 文件的優化、資源管理和環境變量注入,它做用於整個構建過程java

  • clean-webpack-plugin: 打包前自動清理 dist 目錄
  • html-webpack-plugin:自動生成 html,並將打包後的 js 插入進去
  • mini-css-extract-plugin: 把 css 提取成單獨的文件,與 style-loader 功能互斥,不能同時使用
  • 關於代碼壓縮:

JS文件的壓縮 :默認開啓了內置的 terser-webpack-plugin,webpack 在打包時會自動壓縮 js 代碼
css文件的壓縮 :使用 optimize-css-assets-webpack-plugin ,同時使用 cssnano(處理器)node

plugins: [
   new OptimizeCssAssetsPlugin({
     assetNameRegExp: /\.css$/g,
     cssProcessor: require('cssnano')
   })
 ]
複製代碼

html文件的壓縮:修改 html-webpack-plugin,設置壓縮參數react

plugins: [
  new HtmlWebpackPlugin({
    template: path.join(__dirname, 'src/index.html'),
    filename: 'index.html',
    chunks: ['main', 'other'], //要包含哪些chunk
    inject: true, //將chunks自動注入html
    minify: { // 壓縮相關
      html5: true,
      collapseWhitespace: true, //壓縮空白字符
      preserveLineBreaks: false,
      minifyCSS: true,
      minifyJS: true,
      removeComments: true
    }
  })
]
複製代碼

webpack性能優化

開發環境性能優化

模塊熱替換(HotMoudleReplacement)

開啓 dev-server 後默認只要有一個文件變化,會從新構建,刷新瀏覽器頁面。jquery

模塊熱替換: 只從新打包變動的模塊,局部刷新,保留數據狀態,(而不是將全部模塊從新打包,刷新頁面)提高構建速度,使開發更加方便webpack

經過 devServer.hot 啓用,其內部依賴 webpack.HotModuleReplacementPlugin 實現,HotModuleReplacementPlugin 會在 hot: true時自動被引入,能夠不寫

  • 樣式文件:能夠直接使用 HMR 功能,由於 style-loader 內部實現了
  • js 文件:默認不能使用 HMR 功能,須要修改 js 代碼,添加支持 HMR 功能的代碼
if(module.hot){  // 若是開啓了HMR功能
	// 監聽xxx.js文件的變化,一旦發生變化,其餘模塊不會從新打包,會執行回調函數
	module.hot.accept('./xxx.js', function(){ 
    	fn()
    })
}
複製代碼
  • html文件:沒有熱替換,也沒有熱更新,熱更新能夠經過在入口文件添加 html 文件路徑來打開,但一般沒有必要

使用source-map

因爲通過 webpack 打包後的代碼是通過各類 loadersplugins 轉換事後的一個大的js文件,開發過程當中沒法調試。source-map 是一種提供源代碼到構建後代碼映射的技術,報錯時經過 source map 能夠定位到源代碼。
啓用方式:

module.exports = {
  devtool: 'source-map' 
}
複製代碼

選項:
[inline- | hidden- | eval-] [nosources- ] [cheap- [module- ]]source-map

  • source-map: 產生.map 文件,提供錯誤代碼準確信息和源代碼的錯誤位置
  • inline:內聯,將.map 做爲 DataURI 嵌入,不單獨生成 .map 文件,構建速度更快
  • eval:內聯,使用 eval 包裹模塊代碼,指定模塊對應文件
  • cheap:只精確到行,不精確到列
  • module:包含 loadersourcemap

推薦組合:

  • 開發環境:速度快,調試更友好

eval-source-map (eval 速度最快,source-map 調試最友好)

  • 生產環境:內聯會讓代碼體積變大,因此生產環境不用內聯

1.考慮是否要隱藏源代碼?
nosources-source-map ---所有隱藏
hidden-source-map ---只隱藏源代碼,會提示構建後代碼錯誤信息
2.考慮是否要調試友好?
source-map

生產環境性能優化

使用文件指紋進行版本控制和緩存

當設置了 http 強緩存,好比有效期爲一天,若是不使用 hash,當這個文件改變了,由於文件名沒變,因此客戶端使用的仍是舊的緩存;若是使用了 hash,這時文件名就改變了,就會請求新的資源,而沒有更改過的文件繼續使用緩存

  • hash: 構建的 hash,每次構建都會改變,不建議使用
  • chunkhash: 和 webpack 打包的 chunk有關,不一樣的 entry 會生成不一樣的 chunkhash
  • contenthash: 根據文件內容來定義hash,文件內容不變,則 contenthash 不變,推薦在 css 文件上使用

js 文件的指紋設置:

//設置 output 的 filename,使用 [chunkhash]
module.exports = {
  output: {
    filename: '[name][chunkhash:8].js',
    path:__dirname+'/dist'
  }
}
複製代碼

css 文件的指紋設置:

使用 MiniCssExtractPlugin cssjs 中提出來,而後使用 [contenthash]

plugins: [
  new MiniCssExtractPlugin({
    filename: '[name][contenthash:8].css'
  })
]
複製代碼

補充:
module、chunk、bundle的區別

  • module:模塊,源代碼中的一個文件就是一個模塊
  • chunk:一個入口文件所依賴的一大塊就是一個 chunk,能夠理解爲一個 entry 對應一個 chunk
  • bundle:打包後的資源,通常來講一個 chunk 就對應一個 bundle,但也能夠經過一些插件進行拆包,把一個大chunk 拆分爲多個 bundle,好比 MiniCssExtractPlugin

tree shaking

tree shaking(搖樹優化):一個模塊可能有多個方法,只要其中的某個方法使用到了,則整個文件都會被打到 bundle 裏面去,tree shaking 就是隻把用到的方法打入bundle,沒用到的方法會在 uglify 階段被擦除掉。

使用:webpack 默認支持,在 .babelrc裏設置 module:false 便可

webpack 會在 production mode 的狀況下默認開啓 tree shaking
要求:必須是 es6 語法,cjs 的方式不支持

tree shaking 原理

DCE:永遠不會被用到的代碼,好比引入了一個方法可是沒調用 或者 if(false){xxx}

利用 ES6 模塊的特色:

  • 只能做爲模塊頂層的語句出現
  • import 的模塊名只能是字符串常量
  • import bindingimmutable

在打包以前靜態的分析文件,在uglify階段刪除無用代碼

code split

將一個大bundle文件拆包,拆包的方案能夠在cacheGroups裏配置

  • splitChunks
// splitChunks默認配置
optimization: {
    splitChunks: {
      chunks: 'all',  // 不管同步引入仍是異步引入
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,  // 匹配node_modules目錄下的文件
          priority: -10   // 優先級配置項
        },
        default: {
          minChunks: 2,  // 至少引用了2次
          priority: -20,   // 優先級配置項
          reuseExistingChunk: true
        }
      }
    }
  }
複製代碼

在默認設置中

  • 會將 node_mudules 文件夾中的模塊打包進一個叫 vendorsbundle
  • 全部引用超過兩次的模塊分配到 default bundle 中 ,能夠經過 priority 來設置優先級。

DLL

將第三方庫和業務基礎包單獨打成一個文件,只在第一次打,或者須要更新依賴的時候打,此後每次就能夠只打本身的源代碼,加快了構建速度。

方法:使用 DLLPlugin 進行分包,DllReferencePluginmanifest.json 引用

分包須要單獨的配置文件:

// webpack.dll.js
module.exports={
  entry: {
    lib: [
      'lodash',
      'jquery'
    ]
  },
  output: {
    filename: '[name]_[chunkhash].dll.js',
    path: path.join(__dirname, 'build/lib'),
    library: '[name]' // 打包後對外暴露的全局變量名稱
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]', // manifest.json中的name,要與ouput.library名一致
      path: path.join(__dirname, 'build/lib/manifest.json'),
    })
  ]
}
複製代碼

package.json 中添加命令對 dll 單獨打包:

"scripts": {
    "dll": "webpack --config webpack.dll.js"
  },
複製代碼

使用 DllReferencePluginmanifest.json 進行引用,告訴 webpack 使用了哪些動態連接庫,不用再打包這裏面的東西

// webpack.prod.js
new webpack.DllReferencePlugin({
  manifest: require('./build/lib/manifest.json')
}),
複製代碼

使用 addAssetHtmlWebpackPlugindll 資源插到 html

// webpack.prod.js
new addAssetHtmlWebpackPlugin([
  {
    filepath: path.resolve(__dirname, './build/lib/*.dll.js'),
    outputPath: 'static', // 將*.dll.js拷貝後的輸出路徑,相對於html文件
    publicPath: 'static' 
  }
])
複製代碼

demo 地址: webpack實踐-dll-plugin分支

使用先後對比:

使用 dllplugin 前,基礎庫打到了 main.js 裏,佔了 160kb 使用後:main.js 只剩 1.23kb

splitChunks 和 dll 的區別

  • splitChunks 是在構建時拆包,dll 是提早構建好基礎庫,打包的時候就不須要打基礎庫了,時間上 dllsplitChunks 快一點
  • dll 須要多配置一個 webpack.dll.config.js ,並且一旦 dll 中的依賴有更新,得走兩遍打包,比 splitChunks 麻煩一些
  • 推薦使用 splitChunks 去提取頁面間的公共 js 文件。DllPlugin 用於基礎包(框架包、業務包)的分離。

多進程打包

使用 thread-loader 開啓多進程打包,加快打包速度!
注意:啓動進程須要大概 600ms ,進程間通訊也有花銷,項目小的話開啓多進程得不償失,因此只有當項目比較大,打包耗時較長的時候才適合使用多進程。

module: {
  rules: [
    {
      test: /.js$/, 
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: 2 //開啓兩個進程
          }
        },
        {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            cacheDirectory: true 
          }     
        }
      ]
    },
  ]
}
複製代碼

多頁面打包通用配置

多頁面打包須要多個入口文件,多個 HtmlWebpackPlugin 產生多個 html。咱們不可能去手寫不少個入口和 HtmlWebpackPlugin

方案: 動態獲取 entry 和設置 html-webpack-plugin 數量

// 核心方法
const setMPA = () => {
  const entry = {};
  const htmlWebpackPlugins = [];

  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
  entryFiles.forEach(entryFile => {
    const match = entryFile.match(/src\/(.*)\/index\.js/);
    const pageName = match && match[1];

    entry[pageName] = entryFile;
    htmlWebpackPlugins.push(
      new HtmlWebpackPlugin({ 
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: [pageName], //要包含哪些chunk
        inject: true, //將chunks自動注入html
        minify: {
          html5: true,
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true,
          removeComments: false
        }
      }),
    )
  })

  return {
    entry,
    htmlWebpackPlugins
  }
}

複製代碼

其餘具體配置見 webpack實踐-mpa-build分支

相關文章
相關標籤/搜索