深入淺出的webpack構建工具---tree shaking打包效能優化(十二)

龍恩0707發表於2018-09-18

閱讀目錄

1. 什麼是tree-shaking?

2. 在webpack中如何使用 tree-shaking 呢?

3. 使用webpack-deep-scope-plugin 優化

1. 什麼是tree-shaking?

在webpack中,tree-shaking的作用是可以剔除js中用不上的程式碼,但是它依賴的是靜態的ES6的模組語法。
也就是說沒有被引用到的模組它是不會被打包進來的,可以減少我們的包的大小,減少檔案的載入時間,提高使用者體驗。

webpack2版本中就開始引入了 tree shaking的概念,它可以在打包時可以忽略哪些沒有被使用到的程式碼。

注意:要讓 Tree Shaking 正常工作的前提是:提交給webpack的javascript程式碼必須採用了 ES6的模組化語法,因為ES6模組化語法是靜態的(在匯入,匯出語句中的路徑必須是靜態的字串)。

2. 在webpack中如何使用 tree-shaking 呢?

在配置程式碼前,我們來看看我們專案中的目錄結構如下:

### 目錄結構如下:
demo1                                       # 工程名
|   |--- dist                               # 打包後生成的目錄檔案             
|   |--- node_modules                       # 所有的依賴包
|   |--- js                                 # 存放所有js檔案
|   | |-- demo1.js  
|   | |-- main.js                           # js入口檔案
|   |--- common                             # js公用的檔案
|   | |-- util.js                           # 公用的util.js檔案
|   |--- webpack.config.js                  # webpack配置檔案
|   |--- index.html                         # html檔案
|   |--- styles                             # 存放所有的css樣式檔案   
|   | |-- main.styl                         # main.styl檔案   
|   | |-- index.styl                        
|   |--- .gitignore  
|   |--- README.md
|   |--- package.json
|   |--- .babelrc                           # babel轉碼檔案

webpack.config.js 程式碼如下:

const path = require('path');

// 引入 mini-css-extract-plugin 外掛 
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// 清除dist目錄下的檔案
const ClearWebpackPlugin = require('clean-webpack-plugin');

const webpack = require('webpack');

// 引入打包html檔案
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 引入HappyPack外掛 
const HappyPack = require('happypack');

module.exports = {
  // 入口檔案
  entry: {
    main: './js/main.js'
  },
  output: {
    filename: '[name].[contenthash].js',
    // 將輸出的檔案都放在dist目錄下
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        // 使用正則去匹配
        test: /\.styl$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {}
          },
          {
            loader: 'postcss-loader',
            options: {
              ident: 'postcss',
              plugins: [
                require('postcss-cssnext')(),
                require('cssnano')(),
                require('postcss-pxtorem')({
                  rootValue: 16,
                  unitPrecision: 5,
                  propWhiteList: []
                }),
                require('postcss-sprites')()
              ]
            }
          },
          {
            loader: 'stylus-loader',
            options: {}
          }
        ]
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'happypack/loader?id=css-pack'
        ]
      },
      {
        test: /\.(png|jpg)$/,
        use: ['happypack/loader?id=image']
      },
      {
        test: /\.js$/,
        // 將對.js檔案的處理轉交給id為babel的HappyPack的實列
        use: ['happypack/loader?id=babel'],
        // loader: 'babel-loader',
        exclude: path.resolve(__dirname, 'node_modules') // 排除檔案
      }
    ]
  },
  resolve: {
    extensions: ['*', '.js', '.json']
  },
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    port: 8081,
    host: '0.0.0.0',
    headers: {
      'X-foo': '112233'
    },
    inline: true,
    overlay: true,
    stats: 'errors-only'
  },
  mode: 'development',
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html' // 模版檔案
    }),
    new ClearWebpackPlugin(['dist']),

    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:8].css'
    }),
    /****   使用HappyPack例項化    *****/
    new HappyPack({
      // 用唯一的識別符號id來代表當前的HappyPack 處理一類特定的檔案
      id: 'babel',
      // 如何處理.js檔案,用法和Loader配置是一樣的
      loaders: ['babel-loader']
    }),
    new HappyPack({
      id: 'image',
      loaders: [{
        loader: require.resolve('url-loader'),
        options: {
          limit: 10000,
          name: '[name].[ext]'
        }
      }]
    }),
    // 處理styl檔案
    new HappyPack({
      id: 'css-pack',
      loaders: ['css-loader']
    })
  ]
};

.babelrc 配置如下:

{
  "plugins": [
     [
      "transform-runtime",
      {
        "polyfill": false
      }
     ]
   ],
   "presets": [
     [
       "env",
       {
         "modules": false   // 關閉Babel的模組轉換功能,保留ES6模組化語法
       }
     ],
     "stage-2"
  ]
}

common/util.js 程式碼如下:

export function a() {
  alert('aaaa');
}

export function b() {
  alert('bbbbb');
}

export function c() {
  alert('cccc');
}

js/main.js 程式碼如下:

import { a } from '../common/util';

a();

執行 webpack後,打包檔案如下:

然後繼續檢視 dist/main.xxx.js程式碼如下:

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return a; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return b; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "c", function() { return c; });


function a() {
  alert('aaaa');
}

function b() {
  alert('bbbbb');
}

function c() {
  alert('cccc');
}

/***/ }),

如上程式碼,還是會包含 b,c 兩個函式程式碼進來,那是因為 webpack 想要使用tree-shaking功能的話,我們需要壓縮程式碼,就能把沒有引用的程式碼剔除掉,因此我們需要在webpack中加上壓縮js程式碼如下:

// 引入 ParallelUglifyPlugin 外掛
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: [
    // 使用 ParallelUglifyPlugin 並行壓縮輸出JS程式碼
    new ParallelUglifyPlugin({
      // 傳遞給 UglifyJS的引數如下:
      uglifyJS: {
        output: {
          /*
           是否輸出可讀性較強的程式碼,即會保留空格和製表符,預設為輸出,為了達到更好的壓縮效果,
           可以設定為false
          */
          beautify: false,
          /*
           是否保留程式碼中的註釋,預設為保留,為了達到更好的壓縮效果,可以設定為false
          */
          comments: false
        },
        compress: {
          /*
           是否在UglifyJS刪除沒有用到的程式碼時輸出警告資訊,預設為輸出,可以設定為false關閉這些作用
           不大的警告
          */
          warnings: false,

          /*
           是否刪除程式碼中所有的console語句,預設為不刪除,開啟後,會刪除所有的console語句
          */
          drop_console: true,

          /*
           是否內嵌雖然已經定義了,但是隻用到一次的變數,比如將 var x = 1; y = x, 轉換成 y = 5, 預設為不
           轉換,為了達到更好的壓縮效果,可以設定為false
          */
          collapse_vars: true,

          /*
           是否提取出現了多次但是沒有定義成變數去引用的靜態值,比如將 x = 'xxx'; y = 'xxx'  轉換成
           var a = 'xxxx'; x = a; y = a; 預設為不轉換,為了達到更好的壓縮效果,可以設定為false
          */
          reduce_vars: true
        }
      }
    })
  ]
}

再執行下打包命令後。我們繼續檢視程式碼,如下所示:

可以看到還是會把無用的 b函式 和 c函式程式碼打包進去。這是什麼情況?那是因為我們在webpack中配置了 mode: 'development',我們現在把它改成 mode: 'production',後,就可以看到只用 a函式了,我們可以到dist目錄下的main.js程式碼內部搜尋下 alert, 就可以看到了,只有一個alert('a')了。說明b函式和c函式被剔除掉了。

tree-shaking 目前的缺陷:

tree-shaking 能夠利用ES6的靜態引入規範,減少包的體積,避免不必要的程式碼引入,但是webpack只能做一點簡單的事情。
比如 我現在在main.js程式碼改成如下:

import { func2 } from '../common/util';

var a = func2(222);

alert(a);

common/util.js 程式碼如下:

import lodash from 'lodash-es'

var func1 = function(v) {
  alert('111');
  return lodash.isArray(v);
}

var func2 = function(v) {
  return v;
};

export {
  func1,
  func2
}

如上程式碼,在main.js中引入了 func2, 但是並沒有引入func1, 但是func1引入了lodash-es。webpack在檢查的時候發現func1中確實用到了lodash-es,因此不會把lodash去掉,但是func1函式會去掉的。但是我們在js中也並沒有使用到lodash。因此在這種情況下,webpack中的 tree-shaking 解決不了這種情況,因此 webpack-deep-scope-plugin 外掛就可以解決這種問題了,如下沒有使用 webpack-deep-scope-plugin 外掛打包後的檔案大小。如下:

如上main.js 打包壓縮後的js程式碼大小有81.1kb。開啟dist/main.js程式碼搜尋 lodash後,可以搜尋到,因此lodash外掛被打包進去main.js中了,但是實際上我們專案並沒有使用到lodash,因此lodash的庫我們按常理來講並不需要打包進去的。

3. 使用webpack-deep-scope-plugin 優化

該外掛 的github上的程式碼

1. 首先需要安裝 webpack-deep-scope-plugin, 安裝命令如下:

npm i -D webpack-deep-scope-plugin

在webpack.config.js 程式碼引入如下:

// 引入 webpack-deep-scope-plugin 優化
const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default;

module.exports = {
  plugins: [
    new WebpackDeepScopeAnalysisPlugin()
  ]
}

然後我們繼續打包如下所示:

打包後發現如上,只有969位元組,1kb都不到,再開啟dist/main.js 檢視程式碼,搜尋下 lodash, 發現搜尋不到。

注意點:
1. 要使用 tree-shaking,必須保證引用的外掛的模組是ES6模組規範編寫的,這也是我為什麼引用了的是 lodash-es,而不是 'lodash', 如果引用的是lodash的話,是不能去掉的。

2. 在 .babelrc 中,babel設定 module: false, 避免babel將模組轉換為成 CommonJS規範。引入模組包也必須符合ES6規範的。如下 babelrc程式碼:

{
  "plugins": [
     [
      "transform-runtime",
      {
        "polyfill": false
      }
     ]
   ],
   "presets": [
     [
       "env",
       {
         "modules": false   // 關閉Babel的模組轉換功能,保留ES6模組化語法
       }
     ],
     "stage-2"
  ]
}

且需要在 package.json 中定義 sideEffect: false, 這也是為了避免出現 import xxx 導致模組內部的一些函式執行後影響全域性環境, 卻被去除掉的情況.

3. webpack-deep-scope-plugin 外掛依賴 node8.0+ 和 webpack 4.14.0 +

檢視github上的demo

相關文章