Webpack(十二):tree shaking打包性能優(yōu)化

1. 什么是tree-shaking?

webpack中,tree-shaking的作用是可以剔除js中用不上的代碼宦棺,但是它依賴的是靜態(tài)的ES6的模塊語(yǔ)法瓣距。
也就是說(shuō)沒(méi)有被引用到的模塊它是不會(huì)被打包進(jìn)來(lái)的,可以減少我們的包的大小代咸,減少文件的加載時(shí)間蹈丸,提高用戶體驗(yàn)。

webpack2版本中就開(kāi)始引入了 tree shaking的概念,它可以在打包時(shí)可以忽略哪些沒(méi)有被使用到的代碼逻杖。

注意:要讓 Tree Shaking 正常工作的前提是:提交給webpack的javascript代碼必須采用了 ES6的模塊化語(yǔ)法慨默,因?yàn)镋S6模塊化語(yǔ)法是靜態(tài)的(在導(dǎo)入,導(dǎo)出語(yǔ)句中的路徑必須是靜態(tài)的字符串)弧腥。

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

在配置代碼前,我們來(lái)看看我們項(xiàng)目中的目錄結(jié)構(gòu)如下:

### 目錄結(jié)構(gòu)如下:
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轉(zhuǎn)碼文件

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$/,
        // 將對(duì).js文件的處理轉(zhuǎn)交給id為babel的HappyPack的實(shí)列
        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實(shí)例化    *****/
    new HappyPack({
      // 用唯一的標(biāo)識(shí)符id來(lái)代表當(dāng)前的HappyPack 處理一類(lèi)特定的文件
      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']
    })
  ]
};

common/util.js 代碼如下:

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

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

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

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();

執(zhí)行 webpack后虾攻,打包文件如下:

然后繼續(xù)查看 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');
}

/***/ }),

如上代碼,還是會(huì)包含 b,c 兩個(gè)函數(shù)代碼進(jìn)來(lái)更鲁,那是因?yàn)?webpack 想要使用tree-shaking功能的話霎箍,我們需要壓縮代碼,就能把沒(méi)有引用的代碼剔除掉澡为,因此我們需要在webpack中加上壓縮js代碼如下:

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

module.exports = {
  plugins: [
    // 使用 ParallelUglifyPlugin 并行壓縮輸出JS代碼
    new ParallelUglifyPlugin({
      // 傳遞給 UglifyJS的參數(shù)如下:
      uglifyJS: {
        output: {
          /*
           是否輸出可讀性較強(qiáng)的代碼漂坏,即會(huì)保留空格和制表符,默認(rèn)為輸出媒至,為了達(dá)到更好的壓縮效果顶别,
           可以設(shè)置為false
          */
          beautify: false,
          /*
           是否保留代碼中的注釋?zhuān)J(rèn)為保留,為了達(dá)到更好的壓縮效果拒啰,可以設(shè)置為false
          */
          comments: false
        },
        compress: {
          /*
           是否在UglifyJS刪除沒(méi)有用到的代碼時(shí)輸出警告信息驯绎,默認(rèn)為輸出,可以設(shè)置為false關(guān)閉這些作用
           不大的警告
          */
          warnings: false,

          /*
           是否刪除代碼中所有的console語(yǔ)句谋旦,默認(rèn)為不刪除剩失,開(kāi)啟后,會(huì)刪除所有的console語(yǔ)句
          */
          drop_console: true,

          /*
           是否內(nèi)嵌雖然已經(jīng)定義了册着,但是只用到一次的變量拴孤,比如將 var x = 1; y = x, 轉(zhuǎn)換成 y = 5, 默認(rèn)為不
           轉(zhuǎn)換,為了達(dá)到更好的壓縮效果甲捏,可以設(shè)置為false
          */
          collapse_vars: true,

          /*
           是否提取出現(xiàn)了多次但是沒(méi)有定義成變量去引用的靜態(tài)值演熟,比如將 x = 'xxx'; y = 'xxx'  轉(zhuǎn)換成
           var a = 'xxxx'; x = a; y = a; 默認(rèn)為不轉(zhuǎn)換,為了達(dá)到更好的壓縮效果摊鸡,可以設(shè)置為false
          */
          reduce_vars: true
        }
      }
    })
  ]
}

再運(yùn)行下打包命令后绽媒。我們繼續(xù)查看代碼蚕冬,如下所示:

可以看到還是會(huì)把無(wú)用的 b函數(shù) 和 c函數(shù)代碼打包進(jìn)去免猾。這是什么情況?那是因?yàn)槲覀冊(cè)趙ebpack中配置了 mode: 'development',我們現(xiàn)在把它改成 mode: 'production',后囤热,就可以看到只用 a函數(shù)了猎提,我們可以到dist目錄下的main.js代碼內(nèi)部搜索下 alert, 就可以看到了,只有一個(gè)alert('a')了。說(shuō)明b函數(shù)和c函數(shù)被剔除掉了锨苏。

tree-shaking 目前的缺陷:

tree-shaking 能夠利用ES6的靜態(tài)引入規(guī)范疙教,減少包的體積,避免不必要的代碼引入伞租,但是webpack只能做一點(diǎn)簡(jiǎn)單的事情贞谓。
比如 我現(xiàn)在在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, 但是并沒(méi)有引入func1, 但是func1引入了lodash-es葵诈。webpack在檢查的時(shí)候發(fā)現(xiàn)func1中確實(shí)用到了lodash-es裸弦,因此不會(huì)把lodash去掉,但是func1函數(shù)會(huì)去掉的作喘。但是我們?cè)趈s中也并沒(méi)有使用到lodash理疙。因此在這種情況下,webpack中的 tree-shaking 解決不了這種情況泞坦,因此 webpack-deep-scope-plugin 插件就可以解決這種問(wèn)題了窖贤,如下沒(méi)有使用 webpack-deep-scope-plugin 插件打包后的文件大小。如下:


如上main.js 打包壓縮后的js代碼大小有81.1kb贰锁。打開(kāi)dist/main.js代碼搜索 lodash后赃梧,可以搜索到,因此lodash插件被打包進(jìn)去main.js中了豌熄,但是實(shí)際上我們項(xiàng)目并沒(méi)有使用到lodash槽奕,因此lodash的庫(kù)我們按常理來(lái)講并不需要打包進(jìn)去的。

3. 使用webpack-deep-scope-plugin 優(yōu)化

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

npm i -D webpack-deep-scope-plugin

在webpack.config.js 代碼引入如下:

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

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

然后我們繼續(xù)打包如下所示:

打包后發(fā)現(xiàn)如上房轿,只有969字節(jié)粤攒,1kb都不到,再打開(kāi)dist/main.js 查看代碼囱持,搜索下 lodash, 發(fā)現(xiàn)搜索不到夯接。

注意點(diǎn):
1. 要使用 tree-shaking,必須保證引用的插件的模塊是ES6模塊規(guī)范編寫(xiě)的纷妆,這也是我為什么引用了的是 lodash-es,而不是 'lodash'盔几, 如果引用的是lodash的話,是不能去掉的掩幢。

2. 在 .babelrc 中逊拍,babel設(shè)置 module: false, 避免babel將模塊轉(zhuǎn)換為成 CommonJS規(guī)范。引入模塊包也必須符合ES6規(guī)范的际邻。如下 babelrc代碼:

{
  "plugins": [
     [
      "transform-runtime",
      {
        "polyfill": false
      }
     ]
   ],
   "presets": [
     [
       "env",
       {
         "modules": false   // 關(guān)閉Babel的模塊轉(zhuǎn)換功能芯丧,保留ES6模塊化語(yǔ)法
       }
     ],
     "stage-2"
  ]
}

且需要在 package.json 中定義 sideEffect: false, 這也是為了避免出現(xiàn) import xxx 導(dǎo)致模塊內(nèi)部的一些函數(shù)執(zhí)行后影響全局環(huán)境, 卻被去除掉的情況.

3. webpack-deep-scope-plugin 插件依賴 node8.0+ 和 webpack 4.14.0 +

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市世曾,隨后出現(xiàn)的幾起案子缨恒,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骗露,死亡現(xiàn)場(chǎng)離奇詭異岭佳,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)萧锉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)珊随,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人柿隙,你說(shuō)我怎么就攤上這事玫恳。” “怎么了优俘?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵京办,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我帆焕,道長(zhǎng)惭婿,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任叶雹,我火速辦了婚禮财饥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘折晦。我一直安慰自己钥星,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布满着。 她就那樣靜靜地躺著谦炒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪风喇。 梳的紋絲不亂的頭發(fā)上宁改,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音魂莫,去河邊找鬼还蹲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛耙考,可吹牛的內(nèi)容都是我干的谜喊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼倦始,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼斗遏!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起楣号,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤最易,失蹤者是張志新(化名)和其女友劉穎怒坯,沒(méi)想到半個(gè)月后炫狱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體藻懒,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年视译,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嬉荆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酷含,死狀恐怖鄙早,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情椅亚,我是刑警寧澤限番,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站呀舔,受9級(jí)特大地震影響弥虐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜媚赖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一霜瘪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧惧磺,春花似錦颖对、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至番捂,卻和暖如春训堆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背白嘁。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工坑鱼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人絮缅。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓鲁沥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親耕魄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子画恰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354