Webpack4: Tree-shaking 深度解析

什么是Tree-shaking

所謂Tree-shaking就是‘搖’的意思,作用是把項目中沒必要的模塊全部抖掉,用于在不同的模塊之間消除無用的代碼,可列為性能優化的范疇。

Tree-shaking早期由rollup實現,后來webpack2也實現了Tree-shaking的功能,但是至今還不是很完備。至于為什么不完備,可以看一下百度外賣的Tree-shaking原理

Tree-shading原理

Tree-shaking的本質用于消除項目一些不必要的代碼。早在編譯原理中就有提到DCE(dead code eliminnation),作用是消除不可能執行的代碼,它的工作是使用編輯器判斷出某些代碼是不可能執行的,然后清除。

Tree-shaking同樣的也是消除項目中不必要的代碼,但是和DCE又有略不相同。可以說是DCE的一種實現,它的主要工作是應用于模塊間,在打包過程中抽出有用的部分,用于完成DCE。

Tree-shaking是依賴ES6模塊靜態分析的,ES6 module的特點如下:

  1. 只能作為模塊頂層的語句出現
  2. import 的模塊名只能是字符串常量
  3. import binding 是 immutable的

依賴關系確定,與運行時無關,靜態分析。正式因為ES6 module的這些特點,才讓Tree-shaking更加流行。

主要特點還是依賴于ES6的靜態分析,在編譯時確定模塊。如果是require,在運行時確定模塊,那么將無法去分析模塊是否可用,只有在編譯時分析,才不會影響運行時的狀態。

Webpack4的Tree-shaking

webpack從第2版本就開始支持Tree-shaking的功能,但是至今也并不能實現的那么完美。凡是具有副作用的模塊,webpack的Tree-shaking就歇菜了。

副作用

副作用在我們項目中,也同樣是頻繁的出現。知道函數式編程的朋友都會知道這個名詞。所謂模塊(這里模塊可稱為一個函數)具有副作用,就是說這個模塊是不純的。這里可以引入純函數的概念。

對于相同的輸入就有相同的輸出,不依賴外部環境,也不改變外部環境。

符合上述就可以稱為純函數,不符合就是不純的,是具有副作用的,是可能對外界造成影響的。

webpack自身的Tree-shaking不能分析副作用的模塊。以lodash-es這個模塊來舉個例子

//test.js
import _ from "lodash-es";const func1 = function(value){return _.isArray(value);
}
const func2 = function(value){return value=null;
}export {func1,func2,
}
//index.js
import {func2} from './test.js'
func2()
復制代碼

上述代碼在test.js中引入lodash-es,在func1中使用了loadsh,并且這里不符合純函數的概念,它是具有副作用的。func2是一個純函數。

在index.js中只引入了func2,并且使用了func2,可見整個代碼的執行是和func1是沒有任何關系的。我們通過生產環境打包一下試試看(Tree-shaking只在生產環境生效)

main.js 91.7KB,可見這個結果是符合我們的預期的,因為func1函數的副作用,webpack自身的Tree-shaking并沒有檢測到這里有沒必要的模塊。解決辦法還是用的,webpack的插件系統是很強大的。

webpack-deep-scope-plugin

webpack-deep-scope-plugin是一位中國同胞(學生)在Google夏令營,在導師Tobias帶領下寫的一個webpack插件。(此時慢慢的羨慕)

這個插件主要用于填充webpack自身Tree-shaking的不足,通過作用域分析來消除無用的代碼。

插件的原理

這個插件是基于作用域分析的,那么都有什么樣的作用域?

// module scope start// Block{ // <- scope start
} // <- scope end// Classclass Foo { // <- scope start} // <- scope end// If elseif (true) { // <- scope start} /* <- scope end */ else { // <- scope start} // <- scope end// Forfor (;;) { // <- scope start
} // <- scope end// Catchtry {} catch (e) { // <- scope start} // <- scope end// Functionfunction() { // <- scope start
} // <- scope end// Scopeswitch() { // <- scope start
} // <- scope end// module scope end
復制代碼

對于ES6模塊來說,上面作用域只有function和class是可以被導出的,其他的作用域可以稱之為function和class的子作用域并不能被導出實際上歸屬于父作用域的。

插件通過分析代碼的作用域,進而得到作用域與作用域之間的關系。

分析作用域

分析代碼的作用域的基礎是建立做AST(Abstract Syntax Tree)抽象語法樹上面的。這個可以通過escope來完成。

拿到解析完的AST抽象語法樹,利用圖的深度優先遍歷找到哪些作用域是可以被使用到的,哪些作用域是不可以被使用到的。從而分析作用域之間的關系和導出變量之間的關系。進而執行模塊消除。

插件的不足

JavaScript中還是有一些代碼是不會消去的。

根作用域的引用
import { isNull } from 'lodash-es';export function scope(...args) {return isNull(...args);
}復制代碼

在根作用域引用到的作用域不會被消除。

給變量重新賦值
import _ from "lodash-es";var func1
func1 = function(value){return _.isArray(value);
}
const func2 = function(value){return value=null;
}export {func1,func2,
}
復制代碼

上述代碼中先定義了func1,然后又給func1賦值,這樣缺少了數據流分析,同樣插件也是不可以的。

純函數調用

引用作者的例子

import _curry1 from './internal/_curry1';
import curryN from './curryN';
import max from './max';
import pluck from './pluck';var allPass = /*#__PURE__*/_curry1(function allPass(preds) {return curryN(reduce(max, 0, pluck('length', preds)), function () {var idx = 0;var len = preds.length;while (idx < len) {if (!preds[idx].apply(this, arguments)) {return false;}idx += 1;}return true;});
});
export default allPass;
復制代碼

當一個匿名函數被包在一個函數調用中(IIFE也是如此),那么插件是無法分析的。但是如果加上/*#__PURE__*/注釋的話,這個插件會把這個函數調用當作一個獨立的域,tree-shaking是可以生效的。

探討的一些問題

我們都知道在這個ES6泛濫的時代,ES6的代碼在項目中出現已經很廣泛。(先不考慮線上環境打包成ES5)。上面提到插件的利用作用域來分析。能導出的作用域只有class和funciton。function的情況在上面已經說過,現在來探討一下class的情況。

no plugin

當不使用插件的時候,我們來看一下會不會Tree-shaking,預期是會被Tree-shaking。書寫下面這樣一段簡單的代碼。

class Test{init(value){console.log('test init');}
}
export {Test,
}
復制代碼

我們發現在沒有使用插件的情況下,被Tree-shaking了。預期相同。

no plugin + 副作用

當我們在不適用插件的情況下,并且引入副作用,觀察一下會不會打包,預期是不會打包。書寫下面代碼。

class Test{init(value){console.log('test init');return _.isArray(value);}
}
export {Test,
}
復制代碼

觀察打包結果,main.js 91.7KB,并沒有被Tree-shaking,符合預期的結果。

plugin + 副作用

當我們使用插件并且代碼中存在副作用的情況下,觀察打包情況。由于上面的插件原理的鋪墊,我們預期這次是可以Tree-shaking的。利用上例代碼來測試。

我們觀察可以看出main.js 6.78KB,Tree-shaking生效。

plugin + 副作用 + babel

由于用戶瀏覽器對ES6支持度不夠的原因,線上的代碼不能全是ES6的,有時候我們要把ES6的代碼打包成ES5的,放到線上環境來執行。利用上例代碼來測試。

??? 什么鬼,我沒有用到它,為什么這么大??? 一串懵逼

成也babel,敗也babel

懵逼懵逼,babel成就了線上生產環境,但失去了Tree-shaking優化。我們來看看怎么回事。

沒有副作用的情況

當去除調副作用的時候我們來打包一下。

沒有找到test init Tree-shaking生效。為什么呢?我們使用babel線上工具編譯一下源代碼。

"use strict";function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {return right[Symbol.hasInstance](left); } else {return left instanceof right; } }function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } 
}function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } 
}function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }var Test =/*#__PURE__*/function () {function Test() {_classCallCheck(this, Test);}_createClass(Test, [{key: "init",value: function init(value) {console.log("test init")}}]);return Test;}();
復制代碼

上面可以看到最新的babel和webpack有了契合,在Test立即執行函數的地方使用了 /*#__PURE__*/(忘記可以往上看),讓下面的IIFE變成可分析的,成功了使用了Tree-shaking。

有副作用的情況下

上面探討情況的時候就得知有副作用的情況下,不可以被打包的。ES6編譯代碼如下。

"use strict";function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {return right[Symbol.hasInstance](left); } else {return left instanceof right; } }function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } 
}function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } 
}function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }var Test =/*#__PURE__*/function () {function Test() {_classCallCheck(this, Test);}_createClass(Test, [{key: "init",value: function init(value) {console.log("test init")return _.isArray(value);}}]);return Test;}();
復制代碼

這里雖然bable新版契合了webpack,但是還是有一些問題。自己也沒有找出是哪里除了問題,作者說JavaScript代碼還是有一些是不可以清除的,也許就出現到這里。提供一個作者的插件Demo。

babel的解決方案

無論是ES6,還是ES5,Tree-shaking不能生效的原因總的歸根結底還是因為代碼副作用的問題。可想而知代碼的書寫規范是多么重要。這里我所想出的解決方案有兩種。

1.代碼的書寫規范,盡量避免副作用。

書寫代碼過程中盡量使用純函數的方式來寫代碼,保持書寫規范,不讓代碼有副作用。例如把class類引用的副作用改成純的。

class Test{init(value,_){  //參數引入lodash模塊console.log('test init');return _.isArray(value);}
}
export{Test,
}
復制代碼

可以看出,沒有副作用的ES6代碼編譯成ES5,Tree-shaking也是生效的。

2.靈活使用ES6代碼

兩套代碼。當瀏覽器支持的時候,就使用ES6的代碼,ES5的代碼。此方案可參考瀏覽器支持ES6的最優解決方案

總結

項目中難免會一些用不到的模塊占位置影響我們的項目,Tree-shaking的出現也為開發者在性能優化方面提供了非常大的幫助,靈活使用Tree-shaking才能讓Tree-shaking發揮作用,處理好項目中代碼的副作用可以使項目更加的完美。

引用文章

webpack 如何通過作用域分析消除無用代碼

Tree-Shaking性能優化實踐 - 原理篇

原文發布于Webpack4:Tree-shaking深度解析

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/449466.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/449466.shtml
英文地址,請注明出處:http://en.pswp.cn/news/449466.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

真格量化——GFTD策略

#!/usr/bin/env python # coding:utf-8 from PoboAPI import * import datetime import numpy as np import math#開始時間,用于初始化一些參數 def OnStart(context) :print "I\m starting..."#設置全局變量g.signalcount = 0 g.signal_list = []g.buysignal = 0 g…

解決org.hibernate.loader.custom.NonUniqueDiscoveredSqlAliasException: Encountered a duplicated sql ...

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 報錯如題&#xff1a; org.hibernate.loader.custom.NonUniqueDiscoveredSqlAliasException: Encountered a duplicated sql alias [id]…

Spring WebFlux 響應式編程學習筆記(一)

各位Javaer們&#xff0c;大家都在用SpringMVC吧&#xff1f;當我們不亦樂乎的用著SpringMVC框架的時候&#xff0c;Spring5.x又悄(da)無(zhang)聲(qi)息(gu)的推出了Spring WebFlux。web? 不是已經有SpringMVC這么好用的東西了么&#xff0c;為啥又冒出個WebFlux&#xff1f;…

Django Rest Framework -解析器

基本代碼結構 urls.py rom django.conf.urls import url, include from web.views.s5_parser import TestViewurlpatterns [url(rtest/, TestView.as_view(), nametest), ]views.py from rest_framework.views import APIView from rest_framework.response import Response f…

真格量化——菜粕策略

#!/usr/bin/env python # coding:utf-8 from PoboAPI import * import datetime import time import numpy as np from copy import *#開始時間,用于初始化一些參數 def OnStart(context) :context.myacc = None#登錄交易賬號if context.accounts["回測期貨"].Login…

PostgreSQL查看版本信息

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1.查看客戶端版本 psql --version 1 2.查看服務器端版本 2.1 查看詳細信息 select version(); 1 2.2 查看版本信息 show server_ve…

U盤版便攜式Linux制作, casper-rw 解析

一直都在想&#xff0c;不管用誰的電腦&#xff0c;我都可以得到一個完全一致的工作環境&#xff0c;上面有我喜愛的軟件&#xff0c;有我保存的重要資料&#xff0c;甚至瀏覽器的各種偏好都得一模一樣&#xff01;現在的云計算技術可以部分解決這個問題&#xff0c;但是遠遠不…

真格量化-50ETF期權波動率策略

#!/usr/bin/env python # coding:utf-8 from PoboAPI import * import datetime import time import numpy as np #日線級別 #開始時間,用于初始化一些參數 def OnStart(context) :print("I\m starting...")#設定一個全局變量品種,本策略交易50ETF期權g.code = Get…

canvas反向裁剪技巧

我們都知道在canvas 可以通過clip來實現剪裁功能&#xff0c;其步驟一般是先設置要裁剪的區域&#xff08;路徑&#xff09;&#xff0c;然后通過ctx.clip()的實現裁剪&#xff0c;裁剪之后&#xff0c;后續的繪制只能在裁剪的區域顯示效果&#xff0c;比如如下一段代碼&#x…

set 和select 的區別

簡單賦值是沒有區別的 轉載于:https://www.cnblogs.com/bingyizhihun/p/10597908.html

postgres大版本升級

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。進行升級版本之前請一定做好備份&#xff01;查看當前版本&#xff1a;[postgresnode1 ~]$ psqlpsql (9.4.4)Type "help" for h…

馬上有錢:揭密25種成為有錢人的方法(圖)

1、做你真正感興趣的事—你會花很多時間在上面&#xff0c;因此你一定要感興趣才行&#xff0c;如果不是這樣的話&#xff0c;你不愿意把時間花在上面&#xff0c;就得不到成功。 2、自己當老板。為別人打工&#xff0c;你絕不會變成巨富&#xff0c;老板一心一意地縮減開支&a…

無人承運平臺系統流程圖

轉載于:https://www.cnblogs.com/procedureMonkey/p/10598052.html

Neither the JAVA_HOME nor the JRE_HOME environment variable is defined

Centos7.5 啟動tomcat報錯 報錯: Neither the JAVA_HOME nor the JRE_HOME environment variable is defined At least one of these environment variable is needed to run this program原因:沒有安裝java 解決方法:安裝java yum install java -y轉載于:https://www.cnblogs…

讓自己變成一個上進的人

1.認真設計你的環境2.引入外部監督 求“綁架”3.獲取不確定的反饋4.選擇一條既細密&#xff0c;又永無止境的職業上升臺階。轉載于:https://www.cnblogs.com/Julietma/p/10600241.html

年買筆記本的8個小技巧 最適合自己才最好(組圖)

顯然&#xff0c;智能手機和平板在一定程度上可以替代傳統電腦&#xff0c;讓我們可以隨時隨地上網、使用各種應用。不過&#xff0c;傳統電腦也擁有它的不可替代性&#xff0c;比如移動辦公、視頻編輯、玩游戲&#xff0c;筆記本電腦可能是個更好的選擇。 作為一種成熟的電腦…

MySql查詢系統時間,SQLServer查詢系統時間,Oracle查詢系統時間

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 MySQL查詢系統時間 第一種方法&#xff1a;select current_date&#xff1b; MySQL> select current_date as Systemtime; 第二…

flask同源策略解決辦法及flask-cors只允許特定域名跨域

falsk 同源策略解決辦法&#xff1a; 使用 flask-cors 包 并且 在代碼里 加響應的一行代碼解決。 from flask import Flask, session from flask_cors import CORSapp Flask(__name__) CORS(app, resources{r"/*": {"origins": "*"}}) # 允許…

基本變量和引用變量

基本數據類型作比較&#xff0c;值相等則相等&#xff0c;值不相等則不相等&#xff08;忽略數據類型&#xff09; 引用類型作比較&#xff0c;引用地址相等則相等&#xff0c;否則都是不等的。 基本數據類型&#xff0c;和引用數據類型作比較&#xff0c;是比較值是否相等&…

真格量化-持倉量第n檔賣方主力跟隨策略

#!/usr/bin/env python # coding:utf-8 from PoboAPI import * import datetime import time import numpy as np import pandas as pd #日線級別 #開始時間,用于初始化一些參數 def OnStart(context):print("I\m starting...")#設定一個全局變量品種,本策略交易50E…