深圳幻海软件技术有限公司 欢迎您!

原来项目打包也有这么技巧 - 浅谈 Tree Shaking 机制

2023-02-28

前言身为一位前端工程师或多或少都有听过Webpack这套前端打包工具吧,为了让最终打包的档案不会过于庞大,Webpack可是下了非常多的苦功,例如:利用CodeSplitting产出一个又一个的chunk让网页不会一次载入一份很大JS包。然而今天的文章其实不是要讲CodeSplitting,而是要讲

前言

身为一位前端工程师或多或少都有听过 Webpack 这套前端打包工具吧,为了让最终打包的档案不会过于庞大,Webpack 可是下了非常多的苦功,例如:利用 Code Splitting 产出一个又一个的 chunk 让网页不会一次载入一份很大 JS包。

然而今天的文章其实不是要讲 Code Splitting,而是要讲一个比较深入的原理:Tree Shaking。

什么是 Tree Shaking?

什么是 Tree Shaking?Tree Shaking 就字面上翻译来看就是摇晃树木,在 Webpack 的世界中我们通常都会设定一个 Entry Points 来告诉 webpack 要从哪个文件开始往其他文件进行打包,如果用 Tree 的概念来看就是一个主干配上很多的树枝。

Dynamic Language & Static Language

接下来讲个跟 Tree Shaking 比较无关的小知识,但这个小观念可以帮助我们了解为何要在 JavaScript 上执行 Tree Shaking 并不是我们想像中的那麽容易。

接下来讲个跟 Tree Shaking 比较无关的小知识,但这个小观念可以帮助我们了解为何要在 JavaScript 上执行 Tree Shaking 并在程式语言中有分为 动态语言(Dynamic Language) 以及 静态语言(Static Language),被归类在 Dynamic Language 中比较常见的有 JavaScript、PHP、Python 等语不是我们想像中的那么容易。

在编程语言中有分为 动态语言(Dynamic Language) 以及 **静态语言(Static Language)**,被归类在 Dynamic Language 中比较常见的有 JavaScript、PHP、Python 等语言,至于被归类在 Static Language 比较常见的有 C++、Java 等语言。

在 Dynamic Language 中由于我们可以动态的载入非常多东西,例如 function、object 等,对于 Tree Shaking 来说这种会动态载入的东西实在是太难捉摸了,这也让 Dynamic Language 的 Tree Shaking 很难达到最完美。

Dead Code Elimination

在开始讲 Tree Shaking 原理之前必须要了解一个技术:死码删除(Dea诶 Code Elimination)。

在 ​​compiler​​ 的领域中,为了达到执行时间的优化,在代码编译的过程中 compiler 会将对于最终结果没有影响到的代码删除,进而达到执行时间的优化,这段过程称之为 Dead Code Elimination

乍看之下 Dead Code Elimination 在做的事情好像就是 Tree Shaking 要做到的事情,就是要删除无用的代码,但两者其实还是有著些微的差距,接下来就要讲讲 Tree Shaking 的原理。

Tree Shaking 原理

Tree Shaking 其实是 Dead Code Elimination 的一种新的实现原理,在上面的 Dynamic Language 的观念中提到 Dynamic Language 的特性就是可以动态载入任何东西,因为这个特性让 Dead Code Elimination 相当难实现,因为 complier 永远不知道到底哪些程代码是对最终结果不会有影响的。

所以 Tree Shaking 其实要做到的不会像 Dead Code Elimination 那样死板板的要删除对结果不会有影响的程式码,而是要保留会需要用到的代码,这样也可以达到类似 Dead Code Elimination 的效果,只是两者的原理还是有一些差异,而这就是 Tree Shaking 的原理。

ES6 module v.s commonJS

上面提到 Tree Shaking 的原理最主要的目的就是要保留会需要用到的代码,而这点在早期的 JavaScript 其实是无法实现的,但是在 ES6 诞生后有一个非常重要的概念叫:ES6 modules

由于 ES6 modules 的诞生,我们可以在每个文件的最上方先引用即将会需要用到的东西,所以这些 bunbler 就可以藉由这些​​ import file​​ 很快速的知道可以保留哪些文件,进而达到 Tree Shaking 的效果。

这时候读者可能会有另一个问题了,在 ES6 module 还没诞生以前我们也可以利用 commonJS 来进行 module 的导入,为什麽 ES6 module 可以做到 Tree Shaking 可是 commonJS 无法呢?

其实是因为 ES6 module 有著非常多的特性,让 bundler 可以针对这些特性来进行静态的分析:

  • module 必须要在顶层被 import。
  • module 内部会自动被定义为 strict mode。
  • module name 不能动态改变。
  • module 内容为 immutable 无法在其他文件中被动态新增或删除内容。

因为这些强限制在,所以 ES6 module 就可以让 bundler 做到 Tree Shaking 的效果,而 commonJS 则无法达到此点。

改善 import 与 export 方式

我们都知道 ES6 modules 的 export 方式有分 ​​named export​​​ 以及​​ default export​​,这两种方法适用于不同的使用场景,也会对 Tree Shaking 后的文件内容有著非常大的差别。

default export

named export

乍看之下 ​​default export​​​ 跟​​ named export​​ 在写法上好像没什麽太大的差别(除了直接在项目前面加上 export 的写法比较不一样外),最终都是需要用一个物件来包装输出,但两者在 Tree Shaking 后的结果可是有著蛮大的差别,接下来就看来一下 Tree Shaking 过后的结果吧!

default export 经由 Tree Shaking 后的结果:

named export 经由 Tree Shaking 后的结果:

可以看到上面两张图,虽然 Tree Shaking 都有把 multiply 这个 function 移除了,可是 ​​default export​​​ 相较于 ​​named export​​​ 还是新增了不少变量来处理 ​​function parameter​​,这样就不是一个完美的性能优化。

所以假如读者在开发时确定一个文件会需要同时输出很多项目,不管是对象也好函数也罢,这时候都建议用 ​​named export​​ 的方式进行输出这样才能达到最好的性能优化。

改善第三方组件的 import 方式

最后再来看一下 import 第三方组件的最佳方式,在前端开发的过程中为了不要重复照轮子很多时候都会使用大神所开发好的第三方组件来加速开发,但第三方组件的 ​​import​​ 方式其实也会影响到最终的 bundle size

接下将以 ant design 这套 UI library 来进行说明。

首先是利用官方文档的说明来进行 import,其实 antd 本身就有针对其 module 进行 Tree Shaking 的性能优化,所以我们原则上是可以放心的使用官方文档的教学进行 ​​import​​​ 的,接下来我们利用 ​​webpack-bundle-analyzer​​ 来进行档案分析。

可以发现 antd 的文件大小高达 ​​842.15KB​​​,而且裡面还跑出了许多跟 Button 无关的 component 文件,这显然是一个不好的 ​​import​​​ 方式,没想到照著官文档的方式进行 ​​import​​ 也没办法达到最好的性能优化。

但这其实也不是 antd 的错, antd 本身就有做好 Tree Shaking 的动作,详细的说明可以参考 antd 的官方文件,但是这边的事例故意没有在项目的 bundler 设定档中开启 Tree Shaking 的功能,进而导致 antd 的 Tree Shaking 失效。

虽然 bundler 没有开启 Tree Shaking 功能让整体的 bundle size 过大,但我们其实也可以自己手动做这件事,这时候只要我们改成从 antd 的 es folder 进行元件的单独 ​​import​​ 就可以让最终的 bundle size 差非常多,写法如下。

接著我们一样使用 webpack-bundle-analyzer 来进行项目分析。

可以发现整个 antd 的文件大小少了非常多,只剩下 ​​74.8KB​​​ 而且与 Button 无关的其他 component 都没出现了,所以同一种第三方组件不同的 import 方式真的会让整体的性能差距非常大,这个就是比较好的第三方组件 ​​import​​ 方式。

package.json 中的 sideEffects

在 Webpack 的 Tree Shaking 配置中,有一个可以在 ​​package.json​​​ 中配置的叫 ​​sideEffects​​​,这个 ​​sideEffects​​ 的配置主要是让 Webpack 这种 bundler 知道此项目是否可以做 Tree Shaking 的动作。

假如设定为 ​​false​​​ 就代表可以将所有的文件进行 Tree Shaking,若读者知道有哪些档案是不能做 Tree Shaking 的,这时候只要在 ​​sideEffects​​ 内用一个数组将不能做 Tree Shaking 的文件路径写上去,这时候 bundler 就只会针对这个数组以外的文件进行 Tree Shaking。

Webpack 中的 usedExports

在 Webpack 的官方文件中要达到 Tree Shaking 的效果除了在 ​​package.json​​​ 中加上 ​​sideEffects​​​ 外,还可以使用 ​​usedExports​​。

在官方文件中有這麼一段說明:

如果说 sideEffects 在做的事情是把不能做 Tree Shaking 的树枝移除,那 ​​usedExports​​ 在做的事情就是把树枝上没有用到的树叶移除,所以 usedExports 其实才是在做真正的 Tree Shaking。

useExports 利用 terser 这套工具进行项目的 side effects 侦测,假如打包过程中发现此弎既没有 side effects 且某些代码又没有被引用到,则该代码就会在之后的 ​​uglify​​ 被移除,藉此达到真正的 Tree Shaking 效果。

而 usedExports 的设定方式也非常简单,只要在 Webpack 的配置文件中,在 ​​optimization​​​ 内加上​​ usedExports: true​​ 这时候就可以将 usedExports 的功能打开,写法如下:

小结

今天介绍了 Tree Shaking 的相关基本观念,虽然说身为一位前端工程师不一定要懂这个概念,毕竟现在很多主流的框架都已经先把 bundler 的相关 ​​config​​ 都写好了,但了解这些工具背后在做的事情也能帮助到自己在开发时可以稍微省思一下要如何改良自己的代码,进而提升整体的打包后的性能。

像是上面提到的 import 与 export 方式,引用第三方组件时可以如何引用达到最小的 bundle
size,有了这些概念在开发时就可以提升整体的性能 ,所以笔者也建议目前正在学习网页开发的读者都可以稍微了解一下 Tree Shaking 的概念喔。