tree-shaking
所谓 tree-shaking 就是 “摇树” 的意思,可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的静态结构特性,例如 import 和 export。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。
webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
例子
tree shaking 会在编译过程中将未使用的代码进行标记,在 uglify 时移除被标记的无效代码,在 mode 设置为 production 时,webpack 会开启压缩和 tree-shaking 等优化,下面的例子如果配置为生产模式,打包后未使用的引用会被移除掉。
1、将 mode 设置为 none(不使用任何打包优化),optimization 配置中增加 usedExports 。
index.js
// add 只是导入没有使用
import { add } from './util.js';
const divElement = document.createElement('div');
divElement.innerHTML = addRes;
divElement.className = 'demo';
document.body.appendChild(divElement);
util.js
// 导出 add 函数
export const add = (a, b) => {
return a + b
}
webpack.config.js
...
module.exports = {
...
mode: 'none', // 不使用任何默认优化选项
optimization: {
// 告诉 Webpack 去决定每一个模块所用到的导出
usedExports: true,
},
};
执行打包命令后,查看 dist/index.js 中的代码。可以看到 index.js 中只做了导入但是没有调用的方法 add 被打了 unused harmony export 的注释。
2、package.json 中增加 sideEffects: false
"sideEffects": false,
webpack.config.js 中移除 optimization 配置,index.js 与 util.js 中内容不变,执行打包命令后,可以看到打包后的 index.js 中 add 函数已经被移除。
sideEffects 与 usedExports
在了解上面两个参数区别之前,我们先来看下函数的副作用。
函数副作用是指函数在正常工作任务之外对外部环境所施加的影响。具体地说,函数副作用是指函数被调用,完成了函数既定的计算任务,但同时因为访问了外部数据,尤其是因为对外部数据进行了写操作,从而一定程度地改变了系统环境。
在上面例子中我们在 index.js 中导入的 util.js 中导出了一个 add 函数,我们来看下这个函数。
export const add = (a, b) => {
return a + b
}
add 函数中接收了 a 和 b 两个参数,函数体中返回了 a 与 b 的和,此函数返回的结果只依赖于传入的值并且没有其他影响,如修改全局变量等操作,则此函数即为无副作用函数。
export const add = (a, b) => {
window.userName = '***'
return a + b
}
上面的 add 函数除了返回了 a 与 b 的和的同时还修改了 window对象中属性的值,则此函数即为有副作用的函数。
简单了解了函数副作用,我们来看下 sideEffects 和 usedExports(更多被认为是 tree shaking)的两种不同的优化方式。
webpack.config.js
...
module.exports = {
...
mode: 'none', // 不使用任何默认优化选项
optimization: {
// 告诉 Webpack 去决定每一个模块所用到的导出
usedExports: true,
// 开启压缩
minimize: true
},
};
index.js 与 util.js 内容保持不变,在 webpack.config.js 的 optimization 配置中增加参数 minimize 为 true,打包成功后查看输出文件,可以看到之前被打了 unused harmony export 注释的代码被移除掉了。
下面我们来写一个带有副作用的方法,看看 usedExports 会如何处理。
index.js
import './util.js';
const divElement = document.createElement('div');
divElement.innerHTML = 'demo';
divElement.className = 'demo';
document.body.appendChild(divElement);
util.js
Array.prototype.custom = function () {
console.log('custom');
};
export const add = (a, b) => {
return a + b;
};
util.js 中修在 Array 原型链上增加了方法,所以是 util.js 是有副作用的,在 index.js 中我们不只导入 add 方法而是导入整个 util.js 文件,执行打包命令后可以查看 dist/index.js 文件中 add 方法已被删除,有副作用的部分依然在打包文件中。
在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。
通过 package.json 的 "sideEffects" 属性,来实现这种方式。如果所有代码都不包含副作用,我们就可以将 sideEffects 标记为 false,来告诉 webpack 它可以跳过安全检测删除未用到的 export。
依然使用上面包含副作用的代码。来看下增加 sideEffects: false 的效果。
package.json
{
// ...
"sideEffects": false,
// ...
}
webpack.config.js
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
}
};
执行打包命令后查看 dist/index.js 文件,可以看到包含副作用的代码也被移除,即 util.js 导入部分被全部移除。
项目中包含副作用的函数被移除在打包后会导致部分功能不可用,所以 sideEffects 支持传入参数,告知 webpack 传入的文件包含副作用不要移除。
上面的例子中修改 package.json 的配置,将包含副作用的文件传入进去。此数组支持简单的 glob 模式匹配相关文件。其内部使用了 glob-to-regexp(支持:*,**,{a,b},[a-z])。如果匹配模式为 *.css,且不包含 /,将被视为 **/*.css。
package.json
{
// ...
"sideEffects": ["./src/util.js" ],
// ...
}
执行打包命令后查看输出文件,可以看到只有导入但是没使用的 add 函数和 Array 方法都被保留了下来。
tree shaking 开启条件
tree shaking 需要使用 ES2015 模块语法(即 import 和 export)才能生效,有时我们会发现只引用了某个库中的一个方法,却把整个库都加载进来了,同时 bundle 的体积也并没有因为 tree shaking 而减少。这可能是该库不是使用 ESModule 模式导出的,所以在使用某个库时,我们尽量引入 es 版,按需加载,这样 tree shaking 才能将无用代码删除掉。
总结
为了利用 tree shaking 的优势, 我们必须
代码分离及懒加载
前端性能优化一直是围绕在每一个前端周围的话题,减少网络请求、减少加载 bundle 体积、外部资源放在 CDN 等等。对于性能优化 webpack 也提供了一些手段。下面让我们来了解下代码分离、缓存和懒加载。
代码分离
代码分离是 webpack 最引人注目的特性之一,试想如果项目中所有代码都打包到一个 bundle 中,那 bundle 的体积将会变大,这对首次访问页面来说,加载资源的请求时间会变长,将影响用户体验。所以前端性能优化的一个方向是将代码按照一定规则拆分到不同的 bundle 中,触发不同的功能加载不同的资源,这样除了减少资源体积外还能增快请求响应速度。不过拆分的粒度大小还是要看实际的项目需求,无限拆分资源包也会造成资源请求过多。所以对于代码分离我们要结合项目情况合理使用,这会极大影响加载时间。
常用的代码分离方法有三种:
入口分离
入口分离即从 webpack 入口文件配置处分离 bundle。这种分离方式根据项目的多个入口拆分出多个 bundle,这是所有分离方式中最简单的分离方式,不过这种方式也存在弊端即 输出的 bundle 会同时包含导入的三方依赖代码(重复代码),在后面的分离方式讲解中会解决这个问题。我们先来看看入口分离的例子。
src/index.js
index.js 文件中引入依赖包 lodash 的 join 方法。创建 div 标签并将内容添加到 body 中。
import { join } from 'lodash';
const divElement = document.createElement('div');
divElement.innerHTML = join(['hello', 'index'], '-');
document.body.appendChild(divElement);
src/main.js
main.js 文件中同样引入依赖包 lodash 的 join 方法。创建 div 标签并将内容添加到 body 中,只是展示内容与 index.js 不一致。
import { join } from 'lodash';
const divElement = document.createElement('div');
divElement.innerHTML = join(['hello', 'main'], '-');
document.body.appendChild(divElement);
webpack.config.js
在 entry 配置中传入两个入口,chunk 的名字分别是 index 和 main。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
...
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
main: './src/main.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
...
new HtmlWebpackPlugin({
template: './index.html',
}),
],
};
执行打包命令后,在 dist 文件夹下可以看到,同时输出了 mian.js 和 index.js 文件,通过配置 entry 实现了代码分离。我们分别打开 dist/index.js 和 dist/main.js 后可以看到两个 bundle 中都包含了 lodash 的源码。
去重分离
在入口分离的基础上,我们继续优化将重复的部分单独分离出一个 bundle,在 entry 中配置 dependOn 选项,这样可以在多个 chunk 之间共享模块。
webpack.config.js
在 entry 配置中将两个入口文件按照对象的形式定义,除定义入口之外,增加了一个 dependOn 选项,传入的 vendor 为共享模块的 chunk 名称。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
...
module.exports = {
...
entry: {
index: {
import: './src/index.js',
dependOn: 'vendor',
},
main: {
import: './src/main.js',
dependOn: 'vendor',
},
vendor: 'lodash'
},
};
执行打包命令后,在 dist 文件夹下除了两个入口文件外还多了一个 vendor.js 文件,文件的内容即为 lodash 源码。HtmlWebpackPlugin 插件将三个 js 文件都注入到了 index.html 中。项目可以正常运行。
通过在入口配置 dependOn 属性,虽然可以实现公共代码抽离,但是还存在一个问题是,在我们的项目中会有很多公共代码,难道我们要手动的都添加到 dependOn 中吗?这将会增加非常多的工作量并且容易出错。这时我们可以考虑使用 webpack 的 SplitChunksPlugin 插件了。
SplitChunksPlugin
SplitChunksPlugin 插件不需要单独安装,是 webpack 提供的开箱即用的插件。我们通过 optimization 配置即可。
默认情况下,SplitChunksPlugin 它只会影响到按需加载的 chunks,不过我们可以通过传入 chunks 属性不同变量来决定影响哪些 chunk 。
webpack 将根据以下条件自动拆分 chunks:
我们使用 SplitChunksPlugin 来拆分下上个例子中的 lodash,关于 SplitChunksPlugin 的各项配置参数含义,感兴趣的伙伴可以查看官网
webpack.config.js
...
module.exports = {
entry: {
index: './src/index.js',
main: './src/main.js',
},
optimization: {
splitChunks: {
// 设置为 all 意味着 chunk 可以在异步和非异步 chunk 之间共享
chunks: 'all',
},
},
...
};
执行打包命令后,在 dist 文件夹下可以看到除了 index.js 和 main.js 外还输出了一个文件,打开文件可以看到内容正是 lodash。
动态导入
动态导入也叫资源异步加载,当模块数量过多时,可以把一些暂时用不到的模块延迟加载,以此来减少用户初次加载页面的体积,后续模块等到恰当的时机再去加载,因此也把这种加载方式叫懒加载。
在 webpack 中提供了两种类似的技术来实现动态加载。import 函数及 require.ensure。require.ensure 是 Webpack 1 支持的异步加载方式,从 Webpack 2 开始引入了 import 函数,并且官方更推荐使用 import 函数的方式实现异步加载,因此下面我们只介绍 import 函数的使用。
通过 import 函数加载的模块会被异步的进行加载,并且返回一个 Primise 对象。
main.js
export const add = (a, b) => {
console.log(a + b);
}
index.js
index 文件中通过 import 函数导入 main 文件内容并赋值给 addFunc,在 index 中通过 setTimeout 定时任务 2 秒钟后执行 addFunc 回调函数,在回调函数返回值中通过 ES6 的对象解构语法获取到 add 函数。
const addFunc = import('./main');
setTimeout(() => {
addFunc.then(({add}) => {
add(3, 8);
});
}, 2000);
webpack.config.js
...
module.exports = {
mode: 'development',
entry: {
index: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
},
...
};
通过 webpack.config.js 配置我们可以看到入口文件只有 index.js,执行打包命令后在生成的 dist 文件夹中可以看到除了入口文件 index.js 外,还生成了一个 src_main_js.js 的文件,内容即为 src/main.js 中内容对应的编译后代码。
通过 import 函数异步加载文件也可以实现拆分代码的功能,这种方式在实际的项目开发中非常实用,非主页面加载的内容都可以通过 import 函数动态加载,在点击到对应页面时在加载相应资源。
上面的例子中还可以进一步优化下异步加载文件的 chunk 名称。其他配置不变,我们修改下 import 导入函数部分。
index.js
const addFunc = import(/* webpackChunkName: "main" */ './main');
setTimeout(() => {
addFunc.then(({add}) => {
add(3, 8);
});
}, 2000);
通过在 import 函数中增加注释 /* webpackChunkName: "main" */ 其中的 main 即为生成的 chunk 名称。这样在项目中通过定义语义化的名称,可以增加代码的可读性。
总结
上面的示例中,我们总结了代码分离的几种方式,分别是入口分离、去重分离、动态导入,其中的动态导入就是我们常说的懒加载。代码分离对于减少主包体积,优化项目加载速度,减少白屏时间来说将非常有用。
阅读量:2015
点赞量:0
收藏量:0