2024年Web前端最新Webpack 实现 Tree shaking 的前世今生,2024年最新面试后多久没回复才是没戏了

最后

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
就答题情况而言,第一问100%都可以回答正确,第二问大概只有50%正确率,第三问能回答正确的就不多了,第四问再正确就非常非常少了。其实此题并没有太多刁钻匪夷所思的用法,都是一些可能会遇到的场景,而大多数人但凡有1年到2年的工作经验都应该完全正确才对。
只能说有一些人太急躁太轻视了,希望大家通过此文了解js一些特性。

并祝愿大家在新的一年找工作面试中胆大心细,发挥出最好的水平,找到一份理想的工作。

webpack 标记代码 + babel 转译 ES5  -->  UglifyJS 压缩删除无用代码 关于最早版本的 Webpack 实现 tree-shaking 可以参考这篇文章 如何在 Webpack 2 中使用 tree-shaking(链接地址见文末参考),掘金也有翻译版,当然如果不愿意花时间考古,也可以看下面这一段总结:

  • UglifyJS 不支持 ES6 及以上,需要用 Babel 将代码编译为 ES5,然后再用 UglifyJS 来清除无用代码;

  • 通过 Babel 将代码编译为 ES5,但又要让 ES6 模块不受 Babel 预设(preset)的影响:配置 Babel 预设不转换 module,对应地配置 Webpack 的 plugins 配置;

  • 为避免副作用,将其标记为 pure(无副作用),以便 UglifyJS 能够处理,主要是 webpack 的编译过程阻止了对类进行 tree-shaking,它仅对函数起作用,后来通过支持将类编译后的赋值标记为 @__PURE__解决了这个问题。

// .babelrc

{

“presets”: [

[“env”, {

“loose”: true, // 宽松模式

“modules”: false // 不转换 module,保持 ES6 语法

}]

]

}

// webpack.config.js

module: {

rules: [

{ test: /.js$/, loader: ‘babel-loader’ }

]

},

plugins: [

new webpack.LoaderOptionsPlugin({

minimize: true,

debug: false

}),

new webpack.optimize.UglifyJsPlugin({

compress: {

warnings: true

},

output: {

comments: false

},

sourceMap: false

})

]

第二阶段:BabelMinify

webpack 标记代码 -->  Babili(即 BabelMinify)压缩删除无用代码 Babili 后来被重命名为 BabelMinify,是基于 Babel 的代码压缩工具,而 Babel 已经通过我们的解析器 Babylon 理解了新语法,同时又在 babili 中集成了 UglifyJS 的压缩功能,本质上实现了和 UglifyJS 一样的功能,但使用 babili 插件又不必再转译,而是直接压缩,使代码体积更小。

一般使用 Babili 替代 uglify 有 Babili 插件式和 babel-loader 预设两种方式。在官方文档最后有说明,Babel Minify 最适合针对最新的浏览器(具有完整的 ES6+ 支持),也可以与通常的 Babel es2015 预设一起使用,以首先向下编译代码。

在 webpack 中使用 babel-loader,然后再引入 minify 作为一个 preset 会比直接使用 BabelMinifyWebpackPlugin 插件(下一个就讲到)执行得更快。因为 babel-minify 处理的文件体积会更小。

第三阶段:Terser

webpack 标记代码 --> Terser 压缩删除无用代码 (webpack5 已内置) terser 是一个用于 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。如果你看过这个 issue(https://github.com/webpack-contrib/terser-webpack-plugin/issues/15),就会知道放弃 uglify 而投向 terser 怀抱的人越来越多,其原因也很清楚:

  • uglify 不再进行维护且不支持 ES6+ 语法

  • webpack 默认内置配置了 terser 插件实现代码压缩 关于副作用,从  webpack 4 正式版本扩展了未使用模块检测能力,通过 package.json 的 “sideEffects” 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯正 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。

webpack4 的时候还要手动配置一下压缩插件,但最新的  webpack5 已经内置实现 tree-shaking 啦!在生产环境下无需配置即可实现 tree-shaking !

Webpack 的 Tree-shaking 流程


Webpack 标记代码

总的来说,webpack 对代码进行标记,主要是对 import & export 语句标记为 3 类:

  • 所有 import 标记为 /* harmony import */

  • 所有被使用过的 export 标记为/* harmony export ([type]) */,其中 [type] 和 webpack 内部有关,可能是 binding, immutable 等等

  • 没被使用过的 export 标记为/* unused harmony export [FuncName] */,其中 [FuncName] 为 export 的方法名称

首先我们要知道,为了正常运行业务项目,Webpack 需要将开发者编写的业务代码以及支撑、调配这些业务代码的运行时一并打包到产物(bundle)中。落到 Webpack 源码实现上,运行时的生成逻辑可以划分为打包阶段中的两个步骤:

  • 依赖收集:遍历代码模块并收集模块的特性依赖,从而确定整个项目对 Webpack runtime 的依赖列表;

  • 生成:合并 runtime 的依赖列表,打包到最终输出的 bundle。

显然,对代码的语句标记就发生在依赖收集的过程中。

在运行时环境标记所有 import:

const exportsType = module.getExportsType(

chunkGraph.moduleGraph,

originModule.buildMeta.strictHarmonyModule

);

runtimeRequirements.add(RuntimeGlobals.require);

const importContent = /* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n;

// 动态导入语法分析

if (exportsType === “dynamic”) {

runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport);

return [

importContent, // 标记/* harmony import */

/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});\n // 通过 /#PURE/ 注释可以告诉 webpack 一个函数调用是无副作用的

]; // 返回 import 语句和 compat 语句

}

在运行时环境标记所有被使用过的和未被使用的 export:

// 在运行时状态定义 property getters

generate() {

const { runtimeTemplate } = this.compilation;

const fn = RuntimeGlobals.definePropertyGetters;

return Template.asString([

“// define getter functions for harmony exports”,

` f n   =   {fn} =  fn = {runtimeTemplate.basicFunction(“exports, definition”, [

for(var key in definition) {,

Template.indent([

if(${RuntimeGlobals.hasOwnProperty}(definition, key) && !${RuntimeGlobals.hasOwnProperty}(exports, key)) {,

Template.indent([

“Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });”

]),

“}”

]),

“}”

])};`

]);

}

// 输入为 generate 上下文

getContent({ runtimeTemplate, runtimeRequirements }) {

runtimeRequirements.add(RuntimeGlobals.exports);

runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);

const unusedPart =

this.unusedExports.size > 1

? `/* unused harmony exports ${joinIterableWithComma(

this.unusedExports

)} */\n`

: this.unusedExports.size > 0

/* unused harmony export ${first(this.unusedExports)} */\n

: “”;

const definitions = [];

for (const [key, value] of this.exportMap) {

definitions.push(

`\n/* harmony export */   ${JSON.stringify(

key

)}: ${runtimeTemplate.returningFunction(value)}`

);

}

const definePart =

this.exportMap.size > 0

? `/* harmony export */  R u n t i m e G l o b a l s . d e f i n e P r o p e r t y G e t t e r s ( {RuntimeGlobals.definePropertyGetters}( RuntimeGlobals.definePropertyGetters({

this.exportsArgument

}, {${definitions.join(“,”)}\n/* harmony export */ });\n`

: “”;

return ${definePart}${unusedPart}; // 作为初始化代码包含的源代码

}

}

压缩清除大法

UglifyJS

以 UglifyJS 为例,UglifyJS 是一个 js 解释器、最小化器、压缩器、美化器工具集(parser, minifier, compressor or beautifier toolkit)。具体介绍可以查看下 UglifyJS 中文手册。

如果不想浏览这么一大长篇文档,可以看干净利落、直指 tree-shaking 的压缩配置参数总结吧!

  • dead_code – 移除没被引用的代码 // 是不是很眼熟!无用代码!

  • drop_debugger – 移除 debugger

  • unused – 干掉没有被引用的函数和变量。(除非设置"keep_assign",否则变量的简单直接赋值也不算被引用。)

  • toplevel – 干掉顶层作用域中没有被引用的函数 (“funcs”)和/或变量(“vars”) (默认是 false , true 的话即函数变量都干掉)

  • warnings – 当删除没有用处的代码时,显示警告 // 还挺贴心有么有~

  • pure_getters – 默认是 false. 如果你传入 true,UglifyJS 会假设对象属性的引用(例如 foo.bar 或 foo[“bar”])没有函数副作用。

  • pure_funcs – 默认 null. 你可以传入一个名字的数组,UglifyJS 会假设这些函数没有函数副作用。

举个栗子:

plugins: [

new UglifyJSPlugin({

uglifyOptions: {

compress: {

// 这样该函数会被认为没有函数副作用,整个声明会被废弃。在目前的执行情况下,会增加开销(压缩会变慢)。

pure_funcs: [‘Math.floor’]

}

}

})

],

Tip:假如名字在作用域中重新定义,不会再次检测。例如 var q = Math.floor(a/b),假如变量 q 没有被引用,UglifyJS 会干掉它,但 Math.floor(a/b)会被保留,没有人知道它是干嘛的。

  • side_effects – 默认 true. 传 false 禁用丢弃纯函数。如果一个函数被调用前有一段/@PURE/ or /#PURE/ 注释,该函数会被标注为纯函数。例如 /@PURE/foo();

事实上,在这么多的压缩配置中,除了要解决副作用问题要手动配置以外,仅使用 UglifyJS 默认配置即可去除无用标记代码以实现 tree-shaking。

terser

以 terser 为例,terser 是一个用于 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。具体可查看官方文档。虽然没有中文文档,但是一眼扫过去也可以看出来配置参数和 UglifyJS 没有太大区别。当然很明显地多了一些参数:

  • arrows – 如果转换后的代码更短,类和对象字面量方法也将被转换为箭头表达式

  • ecma – 通过 ES2015 或 更高版本来启用压缩选项,将 ES5 代码转换为更小的 ES6+等效形式 显然是因为 terser 支持 ES6+ 语法,这也是它淘汰 UglifyJS 的优势之一。

压缩性能 PK


目前 Webpack 已经更新到了版本 5.X,已经将 terser 插件默认内置且无需配置,虽然生产环境下默认使用 TerserPlugin ,并且也是代码压缩方面比较好的选择,但是还有一些其他可选择项。等等,我们的主题不是 tree-shaking 吗?怎么在压缩工具的路上突然越走越远…

本质上,实现 tree-shaking 的还是压缩工具,所以我们来看压缩工具的性能好像也没毛病!

TIP:压缩是在生产环境中生效的,所以生产环境下才能 tree-shaking。下面 3 个可配置插件要求 webpack 版本至少在 V4+。

UglifyjsWebpackPlugin

基本的使用方式也更加简单:

// webpack.config.js

const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’);

module.exports = {

optimization: {

minimizer: [new UglifyJsPlugin()],

},

};

const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’)

module.exports = {

plugins: [

new UglifyJsPlugin()

]

}

BabelMinifyWebpackPlugin

一般使用 babili 替代 UglifyJS 有 Babili 插件式和 babel-loader 预设两种方式。

Babili 插件式

只要用 Babili 插件替代 uglify 即可,此时也不需要 babel-loader 了:

// webpack.config.js

const MinifyPlugin = require(“babel-minify-webpack-plugin”);

module.exports = {

plugins: [

new MinifyPlugin(minifyOpts, pluginOpts)

]

}

babel-loader 预设
最后

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
就答题情况而言,第一问100%都可以回答正确,第二问大概只有50%正确率,第三问能回答正确的就不多了,第四问再正确就非常非常少了。其实此题并没有太多刁钻匪夷所思的用法,都是一些可能会遇到的场景,而大多数人但凡有1年到2年的工作经验都应该完全正确才对。
只能说有一些人太急躁太轻视了,希望大家通过此文了解js一些特性。

并祝愿大家在新的一年找工作面试中胆大心细,发挥出最好的水平,找到一份理想的工作。

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值