>[success] # 摇树 ~~~ 1.tree-shaking 摇掉代码中未引用部分(dead-code),production模式下会自动使用tree-shaking 打包时除去未引用的代码。作用是优化项目代码 ~~~ >[info] ## 摇树具备条件 ~~~ 1.'Tree Shaking' 是在'编译时'进行无用代码消除的,因此它需要在'编译时确定依赖关系',首先排除'cjs' 他是运行时执行,因此'Tree Shaking' 要配合'ESM',总结: 1.1.只有 ES6 类型的模块才能进行Tree Shaking,因为 ES6 模块的依赖关系是确定的,可以编译时确定依赖 关系,做静态分析 1.2.CommonJS 定义的模块化规范,只有在执行代码后,才能动态确定依赖模块,因此不具备 Tree Shaking 的先天条件 2.在传统编译型语言中,一般由编译器将无用代码在 AST(抽象语法树)中删除,而前端 JavaScript 并没有正统 '编译器'这个概念,那么 Tree Shaking 就需要在工程链中由工程化工具完成。 3.引入方式导致摇树是否生效,以 default 方式引入的模块,无法被 Tree Shaking;而引入单个导出对象的方式, 无论是使用 import * as xxx 的语法,还是 import {xxx} 的语法,都可以进行 Tree Shaking ~~~ >[info] ## 通过webpack 摇树案例 ~~~ 1.下面代码中'components.js' 向外导出三个函数,但是使用的时候在'sec/index.js'只是用了'Button', 剩下两个方法没有使用,因此希望打包时候可以不将这未使用的方法一起打包,这个过程需要 需要通过webpack 提供的'摇树' 2.webpack 本身在mode production 会开启摇树,可以理解是自带行为,但是可以将mode 配置成none, 进行打包后如图一所示未被使用的导出依旧被打包了 ~~~ ~~~ // src/components.js 导出了三个函数,每个函数模拟一个组件 export const Button = () => { return document.createElement('button') console.log('dead-code') // 未引用代码 } export const Link = () => { return document.createElement('a') } export const Heading = level => { return document.createElement('h' + level) } // sec/index.js import { Button } from './components' document.body.appendChild(Button()) ~~~ * 图一 ![](https://img.kancloud.cn/38/59/38590114862e0c5b08a8f15579836c61_924x318.png) >[danger] ##### 配置摇树 ~~~ 1.手动配置webpack摇树,这个案例依旧使用mode为none,但摇树配置项手动配置需要配置'optimization', 先分析摇树过程'先标记','在删除',需要配置属性 1.1.'usedExports' - 打包结果中只导出外部用到的成员(做标记) 1.2.'minimize' - 压缩打包结果(用来删除) 1.3.'concatenateModules'尽可能打包后将每一个模块合并到一个函数中,这样的好处 '既提升了运行效率,又减少了代码的体积',如果看过之前章节可以先webpack导出是以每个模块为单位 配置这个后会将模块打包进一个导出函数中 2.整个过程简单的说Webpack 负责对模块进行分析和标记,而这些压缩插件负责根据标记结果,进行代码删除 3.整个标记分为三类: 3.1.'harmony export',被使用过的 export 会被标记为 harmony export; 3.2.'unused harmony export',没被使用过的 export 标记为 unused harmony export; 3.3.'harmony import',所有 import 标记为 harmony import。 4.整个具体过程 4.1.Webpack 在编译分析阶段,将每一个模块放入 ModuleGraph 中维护; 4.2.依靠 'HarmonyExportSpecifierDependency' 和 'HarmonyImportSpecifierDependency' 分别识别和处理 import 以及 export; 4.3.依靠 'HarmonyExportSpecifierDependency' 识别 'used export' 和 'unused export'。 ~~~ ~~~ module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js' }, // 集中配置webpack的优化功能 optimization: { // 模块只导出被使用的成员 // useExports负责标记枯树叶 usedExports: true, // 尽可能合并每一个模块到一个函数中 concatenateModules: true, // 压缩输出结果 // minimize 负责摇掉枯树枝 minimize: true } } ~~~ * 如图只配置了 useExports 只会做标记并没有删除,当配置了minimize 这些没被使用的将会被删除 ![](https://img.kancloud.cn/ec/60/ec6086f199efa1cf826dc9d8c5259f6e_642x296.png) * 配置concatenateModules,这里没有开启摇树,可以发现之前是按模块导出现在已经将相同模块放到了一起 ![](https://img.kancloud.cn/0b/62/0b62383b85077695a3edf3be1f1f7530_720x456.png) >[info] ## 有的文章说 babel-loader 不能做摇树 ~~~ 1.Tree Shaking 实现前提是使用 ES Modules 组织代码,也就是交给 Webpack 处理的代码必须使用 ESM 实现的模块化 2.有个疑问'Babel' 会不会影响摇树,很多时候我们会选用 babel-loader 去处理JS。而在 Babel 转换代码时, 可能处理掉代码中的 ES Modules 转换成 Common JS,可以通过配置来解决让'Babel' 保留'esm'导入导出 3.在 Babel 7 之前的babel-preset-env中,modules 的默认选项为 'commonjs',因此在使用 babel 处理模块时, 即使模块本身是 ES6 风格的,也会在转换过程中,因为被转换而导致无法在后续优化阶段应用 Tree Shaking。 而在 Babel 7 之后的 @babel/preset-env 中,modules 选项默认为 ‘auto’,它的含义是对 ES6 风格的模块不做 转换(等同于 modules: false),而将其他类型的模块默认转换为 CommonJS 风格。因此我们会看到,后者即 使经过 babel 处理,也能应用 Tree Shaking。 ~~~ ~~~ module.exports = { ... module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ // ['@babel/preset-env'] // 最新版本的babel-loader中自动关闭了ESM转换插件 // ['@babel/preset-env', { modules: 'commonjs' }] // 强制开启该插件 则会导致 Tree Shaking 失效 // ['@babel/preset-env', { modules: false }] // 确保不会开启ESM转换插件 // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换 ['@babel/preset-env', { modules: 'auto' }] ] } } } ] }, optimization: { // 模块只导出被使用的成员 usedExports: true, // 尽可能合并每一个模块到一个函数中 // concatenateModules: true, // 压缩输出结果 // minimize: true } } ~~~ >[info] ## 副作用 ~~~ export function add(a, b) { return a + b } export const memoizedAdd = window.memoize(add) 或者 // 为 Number 的原型添加一个扩展方法 Number.prototype.pad = function (size) { const leadingZeros = Array(size + 1).join(0) return leadingZeros + this } 上面两种写法在打包时候都会产生副作用,其中第一种写法虽然后续可能你没有使用'memoizedAdd ' 但是在打包时候webpack 为了安全起见因为你在window.memoize使用了add 因此也打包进去了 ~~~ >[success] # sideEffects -- 手动配置副作用 ~~~ 1.Webpack4 还新增了 sideEffects 新特性,它允许我们通过配置的方式去标识我们的代码是 否有副作用从而为 Tree Shaking 提供更大的压缩空间。 1.1.'副作用':模块执行时除了导出成员之外所做的事情 2.Tree-shaking 只能移除没有用到的代码成员,而想要完整移除没有用到的模块, 那就需要开启 sideEffects 特性了 ~~~ >[danger] ##### 举个例子 ~~~ 1.在我们写一些项目的时候经常会遇到这种结构,index.js 是所有文件的最后导出文件例如: export { defaultasButton } from'./button' export { defaultasLink } from'./link export { defaultasHeading } from'./heading' 但是在整个项目使用的时候我们导入文件可能最后仅仅只使用的了其中一个'Button' import { Button } from './components' document.body.appendChild(Button()) 这里注意和上面的案例的区别,上面是同一个文件同时导出三个方法,这里是index.js做了一个桥导出三个了文件 使用的时候是在index.js这个桥里面取出对应的方法,这时候使用上面的'摇树的配置'是不会清除掉没有引用的 另外两个文件 2.这里需要使用'sideEffects',来判断引用文件是否存在副作用,也就是是否被使用,就是被导入但没被使用的 就可以不再最后打包文件内容生成 ~~~ * 如图 ![](https://img.kancloud.cn/37/4b/374b13c8373c51b276de9e8c64c4949f_212x313.png) ~~~ 1.使用在'optimization'配置'sideEffects',还需要在package.json中的sideEffects: false 标明项目代码是否有副作用 2.查看 lodash-es 的 package.json 文件,可以看到其中包含了 "sideEffects":false 的描述 ~~~ ~~~ module.exports = { ... optimization: { sideEffects: true, // production 下也会自动开启 } } ~~~ >[danger] ##### 为什么要在package.json 中声明 ~~~ 1.因为有时候会引入css 文件,或者是全局方法文件这里举个例子, 1.1.下面的css文件引入后确实没有地方调用使用它,看似没有副作用,但是实际他不能再打包的时候被删除 1.2.下面js文件中'extend' 在原型上面绑定了一个pad 方法,看似'extend' 也没有地方调用,但是实际产生 了副作用因为他给Number 的原型添加一个扩展方法也是不能再打包的时候删除 2.这种特殊的性质的文件 就不能再'package.json中的sideEffects: false'将所有的看似没有副作用的文件都 删除,需要告诉这些文件看似没有副作用但实际产生了副作用,在package.json 配置如下 { ... "sideEffects": [ // 添加有副作用的文件 "./src/extend.js", "*.css" // 路径通配符的方式 ] } 3.当然直接配置false,表示我们这个项目中的所有代码都没有副作用该删除就删除 "sideEffects": false ~~~ * css 文件 ~~~ // 样式文件属于副作用模块 import './global.css' ~~~ * js 文件 ~~~ // src/extend.js // 为 Number 的原型添加一个扩展方法 Number.prototype.pad = function (size) { // 将数字转为字符串 => '8' let result = this + '' // 在数字前补指定个数的 0 => '008' while (result.length < size) { result = '0' + result } return result } ~~~ ~~~ import './extend' console.log((8).pad(3)) ~~~ >[danger] ##### css ~~~ 1.purgecss-webpack-plugin ~~~ >[success] # Tree Shaking 友好的导出模式 ~~~ export default { add(a, b) { return a + b } subtract(a, b) { return a - b } } 或者 export class Number { constructor(num) { this.num = num } add(otherNum) { return this.num + otherNum } subtract(otherNum) { return this.num - otherNum } } ~~~ ~~~ 1.对于上述情况,以 Webpack 为例,Webpack 将会趋向保留整个默认导出对象/class, 不能把没用到的类或对象内部的方法消除掉,下面三种都将影响 Tree Shaking 1.1.导出一个包含多项属性和方法的对象 1.2.导出一个包含多项属性和方法的 class 1.3.使用export default导出 2.推荐:原子化和颗粒化导出 export function add(a, b) { return a + b } export function subtract(a, b) { return a - b } 这种方式可以让 Webpack 更好地在编译时掌控和分析 Tree Shaking 信息,取得一个更优的 bundle size。 ~~~ >[danger] ##### 关于组价库的优化 https://juejin.cn/post/6844903544760336398 >[danger] ##### 参考文章 [Tree Shaking:移除 JavaScript 上下文中的未引用代码](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=584#/detail/pc?id=5916) [玩转 Webpack 高级特性应对项目优化需求(上)](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=88#/detail/pc?id=2269) [打包提效:如何为 Webpack 打包阶段提速?](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=416#/detail/pc?id=4426)