前言作为一名前端工程师,你或多或少都听说过前端打包工具Webpack。为了让最终打包的文件不会太大,Webpack却下了很多功夫,比如:使用CodeSplitting一个接一个地生成chunk,这样网页就不会一次加载一个很大的JS包。不过,今天的文章不是讲CodeSplitting,而是讲一个更深入的原理:TreeShaking。什么是摇树?什么是摇树?TreeShaking直译为摇树。在Webpack的世界里,我们通常会设置一个EntryPoints来告诉Webpack从哪个文件开始打包其他文件。如果我们用Tree的概念,它就是很多分支上的骨干配置。DynamicLanguage&StaticLanguage接下来我会讲一个和TreeShaking无关的小知识,但是这个小想法可以帮助我们理解为什么在JavaScript上实现TreeShaking并不像我们想象的那么容易。接下来讲一个和TreeShaking相对无关的小知识,但是这个小概念可以帮助我们理解为什么TreeShaking是在JavaScript上实现的,在JavaScript中分为动态语言(DynamicLanguage)和静态语言(StaticLanguage)编程语言。,JavaScript,PHP,Python等比较常见的归类在动态语言中的语言并没有我们想象的那么容易。在编程语言中,有动态语言(DynamicLanguage)和**静态语言(StaticLanguage)**,在动态语言中分为JavaScript、PHP、Python等语言。至于归为静态语言的语言则是C++、Java等比较常见的语言。在DynamicLanguage中,因为我们可以动态加载很多东西,比如function,object等等,对于TreeShaking来说就太难以捉摸了,这使得DynamicLanguage的TreeShaking非常困难。很难达到完美。死代码消除在开始讲TreeShaking的原理之前,必须了解一个技术:死代码消除(DeaCodeElimination)。在编译器领域,为了优化执行时间,编译器会在代码编译过程中删除对最终结果没有影响的代码,从而达到执行时间的优化。这个过程称为死代码消除。乍看之下,DeadCodeElimination所做的似乎就是TreeShaking要做的,也就是删除无用的代码,但两者之间还是有细微的差距。接下来说一下TreeShaking的原理。TreeShaking的原理TreeShaking实际上是一种新的DeadCodeElimination实现原理。上面DynamicLanguage的概念中提到的DynamicLanguage的特点就是可以动态加载任何东西,因为这个特点使得DeadCodeElimination相当难以实现,因为编译器永远不知道哪些代码不会影响最终结果。所以TreeShaking其实需要做的并不是像DeadCodeElimination那样死板的删除不会影响结果的代码,而是保留会用到的代码,这样也能达到类似DeadCodeElimination的效果效果,但是两者在原理上还是有一些区别的,这就是TreeShaking的原理。ES6modulev.scommonJS上面提到的TreeShaking原理主要是为了保留将要使用的代码,这在早期的JavaScript中其实是不可能的,但是在ES6诞生之后,有一个非常重要的概念叫做:ES6模块。由于ES6模块的诞生,我们可以在每个文件的最前面引用即将用到的东西,所以这些bunblers可以通过这些import文件快速知道哪些文件可以保留,进而达到TreeShaking的效果。这时,读者可能会有另一个问题。在ES6模块诞生之前,我们也可以使用commonJS来导入模块。为什么ES6模块可以做TreeShaking而commonJS不能?其实是因为ES6模块有很多特性,所以bundler可以对这些特性进行静态分析:模块必须在顶层引入。该模块自动定义为严格模式。模块名称不能动态更改。模块内容是不可变的,不能在其他文件中动态添加或删除。由于这些强限制,ES6模块可以启用bundler来达到TreeShaking的效果,但是commonJS无法做到这一点。完善进出口方式。我们都知道ES6模块的导出方式分为命名导出和默认导出。这两种方式适用于不同的使用场景,也会对TreeShaking后的文件内容产生影响。有很大的不同。defaultexportnamedexport乍一看,defaultexport和namedexport似乎没有太大的区别(除了直接在项目前面加了export)。最终,两者都需要用一个对象来打包输出,但是两者经过TreeShaking后的结果却大相径庭。让我们看看TreeShaking后的结果吧!TreeShaking后defaultexport的结果:TreeShaking后namedexport的结果:可以看到上面两张图,虽然TreeShaking去掉了multiply的功能,但是相比namedexport还是增加了很多变量来处理函数参数,所以它不是一个完美的性能优化。因此,如果读者在开发的时候确定一个文件会需要同时导出很多项,无论是对象还是函数,建议此时使用具名的导出方式进行输出,以达到最佳性能优化。完善第三方组件的导入方式最后,我们来看看导入第三方组件的最佳方式。在前端开发的过程中,为了不重蹈覆辙,我们经常会使用大神开发的第三方组件来加快开发速度,但是第三方组件的导入方式也会影响最终的bundle大小。接下来将使用antdesignUI库来进行说明。第一种是使用官方文档的描述来导入。其实antd本身就对其模块进行了TreeShaking的性能优化,所以原则上我们可以放心使用官方文档的教学来导入。接下来我们使用webpack-bundle-analyzer进行归档分析。可以发现antd的文件大小高达842.15KB,其中有很多与Button无关的组件文件。这显然是一种糟糕的导入方式。没想到按照官方文档同样的方式导入是没有办法达到最佳性能优化的。但这其实不是antd的错。antd本身就有很好的treeshaking动作。详细说明可以参考antd的官方文档,但是这里的例子故意没有在项目的bundler配置文件中开启treeshaking功能,进而导致antd的TreeShaking失败。虽然bundler没有启用TreeShaking功能使整体bundle体积过大,但实际上我们可以手动完成。这时候只要我们把它从antd的es文件夹改成单独导入components,就可以让最终的bundlesize有很大的不同,写法如下。然后我们也使用webpack-bundle-analyzer进行项目分析。可以发现整个antd的文件体积缩小了很多,只有74.8KB并且没有出现其他与Button无关的组件,所以同一个第三方组件的不同导入方式真的会让人整体性能差距非常大,这是导入第三方组件比较好的方式。package.json中的sideEffects在Webpack的TreeShaking配置中。有一个副作用可以在package.json中配置。这个sideEffects的配置主要是给Webpack这种bundler知道这个项目能不能做TreeShaking的动作。如果设置为false,则表示所有文件都可以进行TreeShaked。如果读者知道哪些文件不能进行TreeShaking,那么只要在sideEffects中使用一个数组,就无法写入TreeShaking的文件路径,那么bundler只会对这个之外的文件进行TreeShaking大批。Webpack中的UsedExports要在Webpack官方文件中实现TreeShaking的效果,除了在package.json中添加sideEffects外,还可以使用usedExports。官方文档中有这样的说法:如果sideEffects做的是去掉不能用于TreeShaking的分支,那么usedExports做的是去掉树枝上不用的叶子,所以usedExports实际上是在做真正的TreeShaking。useExports使用terser来检测项目的副作用。如果在打包的过程中发现这个项目没有副作用,有些代码没有被引用,后面会在uglify中把代码搬走,以达到真正的TreeShaking效果。usedExports的设置方法也很简单,只要在webpack的配置文件中optimization中添加usedExports:true,这时候就可以开启usedExports的功能了,写法如下:今天小结介绍了Tree相关的基本概念摇晃。虽然一个前端工程师不一定需要了解这个概念,毕竟很多主流框架都已经先把bundler的相关config写好了,但是了解这些工具背后到底做了什么也可以帮助你思考如何在开发的时候完善自己的代码,进而提高整体打包后的性能。像上面提到的导入导出方式,在引用第三方组件时如何引用最小bundlesize,有了这些概念,在开发的时候就可以提升整体的性能,所以笔者也推荐目前正在学习web开发的读者们可以了解一点TreeShaking的概念。
