必懂的wenpack優化

wyao發表於2019-07-27

webpack優化

1.production 模式打包自帶優化

  • tree shaking

    tree shaking是一個術語、通常用於打包時移除js中未引用的程式碼(dead-code),它依賴於ES6模組系統中的import 和 export 的靜態結構特性

    開發時引入一個模組時,如果只引用其中一個功能,上線打包時只會把用到的功能打包進bundle中,其他沒有用到的功能都不會打包進來,可以實現最簡單的基本優化

    1. 建立一個 math.js, 丟擲兩個方法
    export const add = (a, b) => a + b
    export const minus = (a, b) => a- b
    1. 在 main.js 中使用
    // tree shaking 分析
    // 若是此時使用 require 引入,不管 math 中的方法是否使用,都會被打包
    const math = require('./utils/math')
    // 若是使用 import 引入, 只會打包使用了 math 的方法
    import { add } from './utils/math'
    console.log('index 頁面',math.add(1,2));
    console.log('index 頁面',add(1,2));
    1. 根據不同的引入方式進行打包,觀察打包後的檔案
  • scope hoisting

    Scope hositing 作用:是將模組之間的關係進行結果推測,可以讓webpack檔案打包出來的程式碼檔案更小、執行的更快

    scope hositing實現原理:分析出模組之間的依賴關係,儘可能的把打散的模組合併到一個函式中,但是前提是不能造成程式碼冗餘, 因此只有哪些被引用了一次的模組可能被合併

    由於scope hositing 需要分析出模組之間的依賴關係,因此原始碼必須使用ES6模組化語句,不然就不能生效,原因和 tree shaking一樣

    1. 在 main.js 中定義幾個變數並輸出

      const a = 1
      const b = 2
      const c = 3
      // webpack 在這裡會進行 預執行,將結果推斷後打包放在這裡
      console.log(a + b + c)
      console.log(a, b, c)
    2. 打包之後程式碼變成

      console.log(6),console.log(1,2,3)

      因為三個變數只是在這個地方定義並且使用,並沒有在其他位置使用,webpack會直接以具體的數值進行打包,節省了三個變數的定義

  • 程式碼壓縮

    所有程式碼使用UglifyJsPlugin進行壓縮、混淆

2.CSS優化

2.1 將CSS提取到獨立檔案中

Mini-css-extract-plugin 是用於將 CSS 提取為獨立的檔案的外掛,對每個包含css的js檔案都會建立一個css檔案,支援按需載入css和sourceMap

  • 只能用於webpack4中,優勢

    • 非同步載入
    • 不重複編譯,效能更好
    • 更容易使用
    • 只針對css
  • 使用

    • 安裝

      npm i -D mini-css-extract-plugin
    • 引用

      const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    • 建立外掛物件,配置抽離的css檔名,支援placeholder語法

      new MiniCssExtractPlugin({
          filename:'[name].css' // [name] 就是 placeholder 語法
      })
    • 將原來配置的所有 style-loader 替換為 MiniCssExtractPlugin.loader

      {
        test:/\.css$/,
        use:[MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test:/\.less$/,
        use:[MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
      },
      {
        test:/\.scss$/,
        use:[MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      },

2.2 自動新增CSS字首

使用 postcss,需要使用 postcss-loader 和 autoprefixer

  1. 安裝

    npm i -D postcss-loader autoprefixer
  2. 修改配置檔案,將 postcss-loader 放置在 css-loader 右邊

    {
      test:/\.css$/,
      use:[MiniCssExtractPlugin.loader, 'css-loader',  'postcss-loader',]
    },
    {
      test:/\.less$/,
      use:[MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
    },
    {
      test:/\.scss$/,
      use:[MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']
    },
  3. 專案根目錄下新增 postcss 的配置檔案: postcss.config.js

    module.exports = {
      plugins: [
        require('autoprefixer')({
          browsers: [
            // 加這個後可以出現額外的相容性字首
            "> 0.01%"
          ]
        })
      ]
    }

2.3 開啟CSS壓縮

需要使用 optimize-css-assets-webpack-plugin 外掛來完成css壓縮

但是由於配置css壓縮時會覆蓋掉webpack預設的優化設定,導致JS程式碼無法壓縮,所以還需要把JS程式碼壓縮外掛倒入進來 terser-webpack-plugin

  1. 安裝

    npm i -D terser-webpack-plugin optimize-css-assets-webpack-plugin
  2. 引用

    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    const TerserPlugin = require('terser-webpack-plugin');
  3. 配置

    optimization:{
      minimizer: [
        new TerserPlugin({}),
        new OptimizeCssAssetsPlugin({})
      ]
    }

webpack4預設採用的JS壓縮外掛是 uglifyjs-webpack-plugin,在 mini-css-extract-plugin上一個版本中還推薦使用該外掛,但是新的版本卻建議使用 terser-webpack-plugin

3.JS優化

code splitting 是webpack打包時用到的重要的優化特性之一、此特效能夠把程式碼分離到不同的bundle中,然後可以按需載入或者並行載入這些檔案,程式碼分離可以用於獲取更小的bundle,以及控制資源載入優先順序,如果能夠合理的使用能夠極大影響載入時間

  • 三種常見的程式碼分離方法
    • 入口起點:使用entry配置,手動的分離程式碼
    • 放置重複:使用 SplitChunksPlugin 去重和分離 chunk
    • 動態匯入:通過模組的行內函數呼叫來分離程式碼

3.1手動配置多入口

  • 手動配置多入口會存在一些問題
    • 如果入口chunks之間包含重複的模組,哪些重複的模組都會被引入到各個打包後的js檔案中
    • 方法不夠靈活,並且不能將核心應用程式邏輯進行動態拆分程式碼
  1. 在webpack配置檔案中配置多個入口

    entry:{
     main: './src/main.js',
     other: './src/other.js'
    },
    output:{
     path: path.join(__dirname, '..', './dist'),
     filename: '[name].js',
     publickPath: '/'
    }
  2. 在main.js 和 other.js 中都共同引入一個模組, 並使用其功能

    1. Main.js

      import $ from 'jquery'
      
      $(() => {
        $('<div></div>').html('main').appendTo('body')
      })
    2. other.js

      import $ from 'jquery'
      
      $(() => {
        $('<div></div>').html('other').appendTo('body')
      })
  3. 打包檔案,可以看到 main 和 other 打包的檔案中都載入的了 jquery

3.2抽取公共程式碼

webpack4 以上使用的外掛為 SplitChunksPlugin,webpack4 之前的使用的 CommonChunkPlugin已經被移除,最新版本的webpack中只需要在配置檔案中的optimization節點下新增一個splitChunks屬性即可進行相關配置

  1. 修改配置檔案

    optimization
      splitChunks:{
        chunks: "all"
      }
    }
  2. 打包檢視檔案

    打包之後會將各自的入口檔案進行打包,額外會再生產一份js檔案,此檔案中就是各個chunk中所引用的公共部分

  • splitChunksPlugin 配置引數

    SplitChunksPlugin 的配置只需要在 optimization 節點下的 splitChunks 進行修改即可,如果沒有任何修改,則會使用預設設定

    預設的 SplitChunksPlugin 配置適用於絕大多數使用者

    • webpack 會基於如下預設原則自動分割程式碼
      • 公用程式碼塊或者來自 node_modules 資料夾的元件模組
      • 打包的程式碼塊大小超過30kb,最小化壓縮之前的
      • 按需載入程式碼塊時,同時傳送的請求最大數量不應該超過5
      • 頁面初始化時,同時傳送的請求最大數量不應該超過3
    • SplitChunksPlugin 預設配置
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'async', // 只對非同步載入的模組進行拆分,import('jquery').then()就是典型的非同步載入,可選項還有 all | initial
          minSize: 30000, // 模組最少大於 30kb 才會拆分
          maxSize: 0, // 為0時模組大小無上限,只要大於 30kb 都會拆分。若是非0,超過了maxSize的值,會進一步拆分
          minChunks: 1, // 模組最少引用一次才會拆分
          maxAsyncRequests: 5, // 非同步載入時同時傳送的請求數量最大不能超過5,超過5的部分不拆分
          maxInitialRequests: 3, // 頁面初始化時,同時傳送的請求數量最大不能超過3,超過3的不跟不拆分
          automaticNameDelimiter: '~', // 預設的連線符
          name: true, // 拆分的chunk名,設定為true表示根據模組名和CacheGroup的key來自動生成,使用上面的連線符連線
          cacheGroups: { // 快取組配置,上面配置讀取完成後進行拆分,如果需要把多個模組拆分到一個檔案,就需要快取,所以命名為快取組
            vendors: { // 自定義快取組名
              test: /[\\/]node_modules[\\/]/, // 檢查 node_modules 目錄,只要模組在該目錄下就使用上面配置拆分到這個組
              priority: -10, // 權重為-10,決定了那個組優先匹配,假如node_modules下面有個模組要拆分,同時滿足vendors和default組,此時就會分到 priority 值比較大的組,因為 -10 > -20 所以分到 vendors 組
              filename:'vendoes.js'
            },
            default: { // 預設快取組名
              minChunks: 2, // 最少引用兩次才會被拆分
              priority: -20, // 權重 -20
              reuseExistingChunk: true // 如果主入口中引入了兩個模組,其中一個正好也引用了後一個,就會直接複用,無需引用兩次
            }
          }
        }
      }
    };

3.3動態匯入(懶載入)

webpack4預設是允許import語法動態匯入的,但是需要babel的外掛支援,最新版babel的外掛包為:@babel/plugin-syntax-dynamic-import,需要注意動態匯入最大的好處就是實現了懶載入,用到那個模組才會載入那個模組,可以提高SPA應用程式的首屏載入速度,三大框架的路由懶載入原理一樣

  1. 安裝

    npm i -D @babel/plugin-syntax-dynamic-import
  2. 修改 .babelrc ,新增 @babel/plugin-syntax-dynamic-import 外掛

    {
     "presets": ["@babel/env"],
     "plugins": [
         "@babel/plugin-proposal-class-properties",
         "@babel/plugin-syntax-dynamic-import"
     ]
    }
  3. 將jq模組動態匯入

    function getDivDom(){
     // import('jquery') 返回的是一個 promise,若是低版本需要注意
     return import('jquery').then(({default: $}) => {
         return $('<div></div>').html('動態匯入')
     })
    }
  4. 給某個按鈕新增點選事件,點選後呼叫getDivDom函式建立元素並新增到頁面

    window.onload = () => {
      document.getElementById('btn').addEventListener('click',() => {
        getDivDom().then(item => {
          item.appendTo('body')
        })
      })
    }

4.noParse

在引入一些第三方模組時,如jq等,我們知道其內部肯定不會依賴其他模組,因為我們用到的只是一個單獨的js或者css檔案,所以此時如果webpack再去解析他們的內部依賴關係,其實是非常浪費時間的,就需要阻止webpack浪費精力去解析這些明知道沒有依賴的庫,可以在webpack的配置檔案的module節點下加上noParse,並配置正則來確定不需要解析依賴關係的模組

module:{
    noParse: /jquery|bootstrap/  // jquery|bootstrap 之間不能加空格變成 jquery | bootstrap, 會無效
}

5.IgnorePlugin

在引入一些第三方模組時,例如momentJS、dayJS,其內部會做i18n處理,所以會包含很多語言包,而語言包打包時會比較佔用空間,如果專案只需要用到中文或者少數語言,可以忽略掉所有的語言包,然後按需引入語言包,從而使得構建效率更高,打包生成的檔案更小

  • 以moment為例

    import moment from 'moment'
    moment.locale('zh-CN') // 設定為中文
    
    console.log(moment().subtract(6, 'days').calendar())
  1. 首先要找到moment依賴的語言包時什麼,通過檢視moment的原始碼來分析

    function loadLocale(name) {
        var oldLocale = null;
        // TODO: Find a better way to register and load all the locales in Node
        if (!locales[name] && (typeof module !== 'undefined') &&
                module && module.exports) {
            try {
                oldLocale = globalLocale._abbr;
                var aliasedRequire = require;
                aliasedRequire('./locale/' + name);
                getSetGlobalLocale(oldLocale);
            } catch (e) {}
        }
        return locales[name];
    }

    通過 aliasedRequire('./locale/' + name) 可以知道momentJS的多語言目錄是locale,所有的語言JS檔案都在這個目錄中

  2. 使用IgnorePlugin外掛忽略其依賴

    將momentJS的多語言目錄locale忽略

    new webpack.IgnorePlugin(/\.\/locale/, /moment/)
  3. 需要使用某些依賴時自行手動引入

    忽略其依賴之後,moment.locale('zh-CN')就會失效,因為其所依賴的語言包全都被忽略了,需要手動將其引入

    import moment from 'moment'
    import  'moment/locale/zh-cn' // 需要手動引入方可生效
    moment.locale('zh-CN')
    
    console.log(moment().subtract(6, 'days').calendar())

6.DLLPlugin

在引入一些第三方模組時,例如Vue、React等,這些框架的檔案一般都是不會修改的,而每次打包都需要去解析他們,也會影響打包速度,就算是做了拆分,也只是提高了上線後的使用者訪問速度,並不會提高構建速度,所以如果需要提高構建速度,應該使用動態連結庫的方式,類似windows的dll檔案

藉助DLLPlugin外掛實現將這些框架作為一個個的動態連結庫,只構建一次,以後的每次構建都只會生成自己的業務程式碼,可以很好的提高構建效率

豬喲思想在於,講一些不做修改的依賴檔案,提前打包,這樣我們開發程式碼釋出的時候就不需要再對這些程式碼進行打包,從而節省了打包時間,主要使用兩個外掛: DLLPlugin和DLLReferencePlugin

需要注意的是,若是使用的DLLPlugin,CleanWebpackPlugin外掛會存在衝突,需要移除CleanWebpackPlugin外掛

  • DLLPlugin

    使用一個單獨webpack配置建立一個dll檔案,並且它還建立一個manifest.json,DLLReferencePlugin使用該json檔案來做對映依賴性,這個檔案會告訴webpack哪些檔案已經提取打包好了

    • 配置引數
      • context(可選):manifest檔案中請求的上下文,預設為該webpack檔案上下文
      • name:公開的dll函式的名稱,和output.library保持一致即可
      • path:manifest.json 生成的資料夾及名稱
  • DLLReferencePlugin

    該外掛主要用於主webpack配置,它引用的dll需要預先構建的依賴該系

    • 配置引數
      • context: manifest檔案中的請求上下文
      • manifest: DLLPlugin外掛生成的manifest.json
      • content(可選): 請求的對映模組id(預設為manifest.content)
      • name(可選): dll暴露的名稱
      • scope(可選): 字首用於訪問dll的檔案
      • sourceType(可選): dll是如何暴露(libraryTarget)

將VUE專案中的庫抽取成DLL

  1. 準備一份將VUE打包成DLL的webpack配置檔案。
  • 在build目錄下新建一個檔案webpack.vue.js,專門用於打包vue的DLL的。
  • 配置入口:將多個要做成dll的庫全放進來
  • 配置出口:一定要設定library屬性,將打包好的結果暴露在全域性
  • 配置plugin:設定打包後dll檔名和manifest檔案所在地
// 此配置檔案 是打包VUE全家桶的
const path = require('path')
const webpack = require('webpack')

module.exports = {
  mode: 'production',
  entry:{
    vue: [ 
      'vue/dist/vue',
      'vue-router'
    ]
  },
  output:{
    path: path.resolve(__dirname, '../dist'),
    filename: '[name]_dll.js',
    library: '[name]_dll' // 最終會在全域性暴露出一個[name]_dll的物件
  },
  plugins:[
    new webpack.DllPlugin({
      name: '[name]_dll',
      path: path.resolve(__dirname, '../dist/manifest.json'),
    })
  ]
}

webpack.vue.js 只是用來打包生成 [name]_dd.js 檔案和 manifest.json檔案的,是不需要參與到業務程式碼打包的,因為只會在每一次修改了需要生成dll檔案的時間才會執行一次,否則不需要參與到打包

  1. webpack.base.js中進行外掛的配置

使用DllReferencePlugin指定manifest檔案的位置即可

new webpack.DllReferencePlugin({
    manifest: path.resolve(__dirname, '../dist/manifest.json'),
})
  1. 由於[name]_dll檔案生成之後,並沒有動態的引入進去,所以需要一個外掛可以動態的將生成的dll檔案引入

安裝add-asset-html-webpack-plugin

npm i -D add-asset-html-webpack-plugin

配置外掛自動新增script標籤到HTML中,需要注意的是,必須在HtmlWebpackPlugin後面引入,因為HtmlWebpackPlugin是生產一個html檔案,AddAssetHtmlWebpackPlugin是在已有的html中注入一個script,否則會被覆蓋

new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, '../dist/vue_dll.js')
})

7.瀏覽器快取

在做了眾多程式碼分離的優化後,其目的是為了更好的利用瀏覽器快取,達到提高訪問速度的效果,所以構建專案時做程式碼分割是必須的,

例如將固定的第三方模板抽離,下次修改了業務程式碼,重新發布上線不重啟伺服器,使用者再次訪問伺服器就不需要再次載入第三方模板了

但是此時會遇到一個問題,如果再次打包上線不重啟伺服器,客戶端會把以前的業務程式碼和第三方模組同時快取,再次訪問時依舊會訪問快取中的業務程式碼,所以會導致業務程式碼也無法更新

需要在output節點的filename中使用placeholder語法,根據程式碼內容生產檔名的hash,之後每次打包業務程式碼時,如果有改變,會生成新的hash作為檔名,瀏覽器就不會使用快取了,而第三方模組不會重新打包生成新的名字,則會繼續使用快取

output: {
  path: path.join(__dirname, '..','./dist'),
  filename:'[name].[contenthash:8].bundle.js',
  publicPath: '/'
},

8.打包分析

專案構建完成後,需要通過一些工具對打包後的bundle進行分析,通過分析可以得到一些有用的資訊

  1. 使用 --profile --josn 引數,以json格式來輸出打包後的結果到某個指定的檔案中
webpack --profile --json > stats.json
  1. 將stats.json檔案放到工具中進行分析
  • 官方工具: analyse

  • webpack-chart:webpack stats 可互動餅圖。

  • webpack-visualizer:視覺化並分析你的 bundle,檢查哪些模組佔用空間,哪些可能是重複使用的。

  • webpack-bundle-analyzer:一個 plugin 和 CLI 工具,它將 bundle 內容展示為便捷的、互動式、可縮放的樹狀圖形式。是一個外掛,可以以外掛安裝到專案中

    • 安裝
    npm i -D webpack-bundle-analyzer
    • 使用, 配置在一個單獨的檔案中 webpack.analyse.js (直接拷貝的web pack.prod.js,僅僅是多了此外掛的使用)
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    module.exports = {
      plugins: [
        new BundleAnalyzerPlugin()
      ]
    }
  • webpack bundle optimize helper:此工具會分析你的 bundle,併為你提供可操作的改進措施建議,以減少 bundle 體積大小。

9.prefetching && preloading

在優化訪問效能時,除了利用瀏覽器快取之外,還需要涉及到一個效能指標: 覆蓋率(coverage rate)

可以在chrome瀏覽器的控制檯中按 ctrl + shift + p,查詢 coverage,開啟覆蓋率皮膚,開始錄製後重新整理頁面,即可看到每個js檔案的覆蓋率,以及總的覆蓋率

想提高覆蓋率,需要儘可能多的使用impor動態匯入,也就是懶載入的功能,將一切能使用懶載入的地方都是用懶載入,這樣可以大大的提高覆蓋率

但是有時候使用懶載入會影響使用者體驗,所以可以在使用懶載入的時候使用魔法註釋(Magic Comments): prefetching,是指在首頁資源載入完畢後,空閒的時候,將動態匯入的資源載入進來,這樣既可以提高首屏載入速度,也可以解決懶載入可能會影響使用者體驗的問題

相關文章