作用域提升

从历史上看,JavaScript 打包器的工作方式是将每个模块封装在一个函数中,该函数在导入模块时调用。这确保每个模块都有单独的隔离作用域和在预期时间运行的副作用,并启用像 热模块更换 这样的开发功能。然而,所有这些单独的功能都是有代价的,无论是在下载大小还是 运行时性能 方面。

¥Historically, JavaScript bundlers have worked by wrapping each module in a function, which is called when the module is imported. This ensures that each module has a separate isolated scope and side effects run at the expected time, and enables development features like hot module replacement. However, all of these separate functions have a cost, both in terms of download size and runtime performance.

在生产构建中,Parcel 在可能的情况下将模块连接到单个作用域中,而不是将每个模块封装在单独的函数中。这称为“范围提升”。这有助于使缩小更加有效,并且还通过使模块之间的引用成为静态而不是动态对象查找来提高运行时性能。

¥In production builds, Parcel concatenates modules into a single scope when possible, rather than wrapping each module in a separate function. This is called “scope hoisting”. This helps make minification more effective, and also improves runtime performance by making references between modules static rather than dynamic object lookups.

Parcel 还静态分析每个模块的导入和导出,并删除所有未使用的内容。这称为 "摇树" 或 "死代码消除"。静态和 动态导入CommonJSES 模块 都支持 Tree Shaking,甚至支持 CSS 模块 的跨语言。

¥Parcel also statically analyzes the imports and exports of each module, and removes everything that isn't used. This is called "tree shaking" or "dead code elimination". Tree shaking is supported for both static and dynamic import, CommonJS and ES modules, and even across languages with CSS modules.

作用域提升的工作原理

#

¥How scope hoisting works

Parcel 的作用域提升实现是通过独立且并行地分析每个模块,最后将它们连接在一起来实现的。为了使连接到单个作用域中是安全的,每个模块的顶层变量都被重命名以确保它们是唯一的。此外,导入的变量会被重命名,以匹配已解析模块中导出的变量名称。最后,删除所有未使用的导出。

¥Parcel’s implementation of scope hoisting works by analyzing each module independently and in parallel, and at the end concatenating them together. In order to make concatenation into a single scope safe, the top-level variables of each module are renamed to ensure they are unique. In addition, imported variables are renamed to match the exported variable names from the resolved module. Finally, any unused exports are removed.

index.js:
import {add} from './math';

console.log(add(2, 3));
math.js:
export function add(a, b) {
return a + b;
}

export function square(a) {
return a * a;
}

编译为类似:

¥Compiles to something like:

function $fa6943ce8a6b29$add(a, b) {
return a + b;
}

console.log($fa6943ce8a6b29$add(2, 3));

如你所见,add 函数已重命名,并且引用已更新以匹配。square 功能已被删除,因为它未被使用。

¥As you can see, the add function has been renamed, and the reference has been updated to match. The square function has been removed because it is unused.

与将每个模块封装在函数中相比,这会导致输出更小、更快。不仅没有多余的函数,而且也没有 exports 对象,并且对 add 函数的引用是静态的而不是属性查找。

¥This results in much smaller and faster output than if each module had been wrapped in a function. Not only are there no extra functions, but there are also no exports objects, and the reference to the add function is static rather than a property lookup.

避免纾困

#

¥Avoiding bail outs

Parcel 可以静态分析许多模式,包括 ES 模块 importexport 语句、CommonJS require()exports 赋值、动态 import() 解构和属性访问等等。然而,当遇到无法提前静态分析的代码时,Parcel 可能必须 "保释" 并将模块封装在函数中,以保留副作用或允许在运行时解析导出。

¥Parcel can statically analyze many patterns including ES module import and export statements, CommonJS require() and exports assignments, dynamic import() destructuring and property accesses, and more. However, when it comes across code that cannot be statically analyzed ahead of time, Parcel may have to "bail out" and wrap the module in a function in order to preserve side effects or allow exports to be resolved at runtime.

要确定树抖动未按预期发生的原因,请使用 --log-level verbose CLI 选项运行 Parcel。这将打印每次发生的救援的诊断信息,包括显示导致救援的原因的代码帧。

¥To determine why tree shaking is not occurring as expected, run Parcel with the --log-level verbose CLI option. This will print diagnostics for each bailout that occurs, including a code frame showing what caused it.

parcel build src/app.html --log-level verbose

动态成员访问

#

¥Dynamic member accesses

Parcel 可以静态解析构建时已知的成员访问,但是当使用动态属性访问时,模块的所有导出都必须包含在构建中,并且 Parcel 必须创建一个导出对象,以便可以在运行时解析该值 。

¥Parcel can statically resolve member accesses that are known at build time, but when a dynamic property access is used, all exports of the module must be included in the build, and Parcel must create an exports object so that the value can be resolved at runtime.

import * as math from './math';

// ✅ Static property access
console.log(math.add(2, 3));

// 🚫 Dynamic property access
console.log(math[op](2, 3));

此外,Parcel 不会跟踪命名空间对象对另一个变量的重新分配。除静态属性访问之外的任何导入命名空间的使用都将导致包含所有导出。

¥In addition, Parcel does not track re-assignments of a namespace object to another variable. Any usage of an import namespace other than a static property access will cause all exports to be included.

import * as math from './math';

// 🚫 Reassignment of import namespace
let utils = math;
console.log(utils.add(2, 3));

// 🚫 Unknown usage of import namespace
doSomething(math);

动态导入

#

¥Dynamic imports

Parcel 支持通过静态属性访问或解构进行树形抖动动态导入。await 和 Promise then 语法都支持这一点。但是,如果以任何其他方式访问从 import() 返回的 Promise,则 Parcel 必须保留已解析模块的所有导出。

¥Parcel supports tree shaking dynamic imports with static property accesses or destructuring. This is supported with both await and Promise then syntax. However, if the Promise returned from import() is accessed in any other way, Parcel must preserve all exports of the resolved module.

注意:对于 await 情况,不幸的是,只有当 await 没有转译掉时(即使用现代 browserslist 配置),才能删除未使用的导出。

¥Note: For the await cases, unused exports can unfortunately only be removed when await is not transpilied away (i.e. with a modern browserslist config).

// ✅ Destructuring await
let {add} = await import('./math');

// ✅ Static member access of await
let math = await import('./math');
console.log(math.add(2, 3));

// ✅ Destructuring Promise#then
import('./math').then(({add}) => console.log(add(2, 3)));

// ✅ Static member access of Promise#then
import('./math').then(math => console.log(math.add(2, 3)));

// 🚫 Dynamic property access of await
let math = await import('./math');
console.log(math[op](2, 3));

// 🚫 Dynamic property access of Promise#then
import('./math').then(math => console.log(math[op](2, 3)));

// 🚫 Unknown use of returned Promise
doSomething(import('./math'));

// 🚫 Unknown argument passed to Promise#then
import('./math').then(doSomething);

CommonJS

#

除了 ES 模块之外,Parcel 还可以分析许多 CommonJS 模块。Parcel 支持对 CommonJS 模块内的 exportsmodule.exportsthis 进行静态分配。这意味着属性名称必须在构建时静态已知(即不是变量)。

¥In addition to ES modules, Parcel can also analyze many CommonJS modules. Parcel supports static assignments to exports, module.exports, and this within a CommonJS module. This means the property name must be known statically at build time (i.e. not a variable).

当看到非静态模式时,Parcel 会创建一个所有导入模块在运行时访问的 exports 对象。所有导出必须包含在最终构建中,并且不能执行树摇动。

¥When a non-static pattern is seen, Parcel creates an exports object that all importing modules access at runtime. All exports must be included in the final build and no tree shaking can be performed.

// ✅ Static exports assignments
exports.foo = 2;
module.exports.foo = 2;
this.foo = 2;

// ✅ module.exports assignment
module.exports = 2;

// 🚫 Dynamic exports assignments
exports[someVar] = 2;
module.exports[someVar] = 2;
this[someVar] = 2;

// 🚫 Exports re-assignment
let e = exports;
e.foo = 2;

// 🚫 Module re-assignment
let m = module;
m.exports.foo = 2;

// 🚫 Unknown exports usage
doSomething(exports);
doSomething(this);

// 🚫 Unknown module usage
doSomething(module);

在导入方面,Parcel 支持静态属性访问和 require 调用的解构。当看到非静态访问时,必须包含已解析模块的所有导出,并且不能执行树摇动。

¥On the importing side, Parcel supports static property accesses and destructuring of require calls. When a non-static access is seen, all exports of the resolved module must be included and no tree shaking can be performed.

// ✅ Static property access
const math = require('./math');
console.log(math.add(2, 3));

// ✅ Static destructuring
const {add} = require('./math');

// ✅ Static property assignment
const add = require('./math').add;

// 🚫 Non-static property access
const math = require('./math');
console.log(math[op](2, 3));

// 🚫 Inline require
doSomething(require('./math'));
console.log(require('./math').add(2, 3));

避免 eval

#

¥Avoid eval

eval 函数在当前作用域内执行字符串中的任意 JavaScript 代码。这意味着 Parcel 无法重命名作用域内的任何变量,以防它们被 eval 访问。在这种情况下,Parcel 必须将模块封装在函数中,并避免缩小变量名称。

¥The eval function executes arbitrary JavaScript code in a string within the current scope. This means Parcel cannot rename any of the variables within the scope in case they are accessed by eval. In this case, Parcel must wrap the module in a function and avoid minifying the variable names.

let x = 2;

// 🚫 Eval causes wrapping and disables minification
eval('x = 4');

如果你需要从字符串运行 JavaScript 代码,你可以改用 函数 构造函数。

¥If you need to run JavaScript code from a string, you may be able to use the Function constructor instead.

避免顶层 return

#

¥Avoid top-level return

CommonJS 允许在模块的顶层(即函数外部)使用 return 语句。当出现这种情况时,Parcel 必须将模块封装在函数中,以便执行仅停止该模块而不是整个包。此外,树摇动被禁用,因为导出可能无法静态得知(例如,如果返回是有条件的)。

¥CommonJS allows return statements at the top-level of a module (i.e. outside a function). When this is seen, Parcel must wrap the module in a function so that execution stops only that module rather than the whole bundle. In addition, tree shaking is disabled because exports may not be known statically (e.g. if the return is conditional).

exports.foo = 2;

if (someCondition) {
// 🚫 Top-level return causes wrapping and disables tree shaking
return;
}

exports.bar = 3;

避免 moduleexports 重新分配

#

¥Avoid module and exports re-assignment

当 CommonJS moduleexports 变量被重新分配时,Parcel 无法静态分析模块的导出。在这种情况下,模块必须封装在函数中,并且树形抖动被禁用。

¥When the CommonJS module or exports variables are re-assigned, Parcel cannot statically analyze the exports of the module. In this case, the module must be wrapped in a function and tree shaking is disabled.

exports.foo = 2;

// 🚫 Exports reassignment causes wrapping and disables tree shaking
exports = {};

exports.foo = 5;

避免有条件的 require()

#

¥Avoid conditional require()

与仅允许在模块顶层使用的 ES 模块 import 语句不同,require 是一个可以从任何地方调用的函数。但是,当从条件语句或另一个控制流语句中调用 require 时,Parcel 必须将解析的模块封装在函数中,以便在正确的时间执行副作用。这也递归地适用于已解析模块的任何依赖。

¥Unlike ES module import statements which are only allowed at the top-level of a module, require is a function that may be called from anywhere. However, when require is called from within a conditional or another control flow statement, Parcel must wrap the resolved module in a function so that side effects are executed at the right time. This also applies recursively to any dependencies of the resolved module.

// 🚫 Conditional requires cause recursive wrapping
if (someCondition) {
require('./something');
}

副作用

#

¥Side effects

许多模块仅包含声明,例如函数或类,但有些模块还可能包含副作用。例如,模块可能会向 DOM 中插入某些内容、将某些内容记录到控制台、分配给全局变量(即 polyfill)或初始化单例。即使模块的导出未使用,也必须始终保留这些副作用,以使程序正常工作。

¥Many modules only contain declarations, like functions or classes, but some may also include side effects. For example, a module might insert something into the DOM, log something to the console, assign to a global variable (i.e. a polyfill), or initialize a singleton. These side effects must always be retained for the program to work correctly, even if the exports of the module are unused.

默认情况下,Parcel 包含所有模块,这可确保始终运行副作用。但是,package.json 中的 sideEffects 字段可用于向 Parcel 和其他工具提示你的文件是否包含副作用。对于将库包含在其 package.json 文件中最有意义。

¥By default, Parcel includes all modules, which ensures side effects are always run. However, the sideEffects field in package.json can be used to give Parcel and other tools a hint about whether your files include side effects. This makes the most sense for libraries to include in their package.json files.

sideEffects 字段支持以下值:

¥The sideEffects field supports the following values:

当文件被标记为无副作用时,如果 Parcel 没有任何使用的导出,则在连接打包包时可以跳过整个文件。这可以显着减少包的大小,特别是当模块在初始化期间调用辅助函数时。

¥When a file is marked as side effect free, Parcel is able to skip the entire file when concatenating the bundle if it does not have any used exports. This can reduce bundle sizes significantly, especially if the module calls helper functions during its initialization.

app.js:
import {add} from 'math';

console.log(add(2, 3));
node_modules/math/package.json:
{
"name": "math"
"sideEffects": false
}
node_modules/math/index.js:
export {add} from './add.js';
export {multiply} from './multiply.js';

let loaded = Date.now();
export function elapsed() {
return Date.now() - loaded;
}

在本例中,仅使用 math 库中的 add 函数。multiplyelapsed 未使用。通常,仍然需要 loaded 变量,因为它包含在模块初始化期间运行的副作用。然而,由于 package.json 包括 sideEffects 字段,因此可以完全跳过 index.js 模块。

¥In this case, only the add function from the math library is used. multiply and elapsed are unused. Normally, the loaded variable would still be needed because it includes a side effect that runs during the module's initialization. However, because the package.json includes the sideEffects field, the index.js module can be entirely skipped.

除了尺寸优势之外,使用 sideEffects 字段还具有构建性能优势。在上面的例子中,因为 Parcel 知道 multiply.js 没有副作用,并且没有使用它的导出,所以它根本就不会被编译。但是,如果使用 export *,则情况并非如此,因为 Parcel 不知道哪些导出可用。

¥In addition to size benefits, using the sideEffects field also has build performance benefits. In the above example, because Parcel knows multiply.js has no side effects, and none of its exports are used, it is never even compiled at all. However, if export * had been used instead, this would not be true because Parcel would not know what exports are available.

sideEffects 的另一个好处是它也适用于打包。如果模块导入 CSS 文件或包含动态 import(),则在未使用该模块时将不会创建包。

¥Another benefit of sideEffects is that it also applies to bundling. If a module imports a CSS file or contains a dynamic import(), the bundle will not be created if the module is unused.

纯注释

#

¥PURE annotations

你还可以使用 /*#__PURE__*/ 注释来注释单个函数调用,这告诉压缩器当结果未使用时可以安全地删除该函数调用。

¥You can also annotate individual function calls with a /*#__PURE__*/ comment, which tells the minifier that it's safe to remove that function call when the result is unused.

export const radius = 23;
export const circumference = /*#__PURE__*/ calculateCircumference(radius);

在此示例中,如果未使用 circumference 导出,则也不会包含 calculateCircumference 函数。如果没有 PURE 注释,calculateCircumference 仍然会被调用,以防它有副作用。

¥In this example, if the circumference export is unused, then the calculateCircumference function will also not be included. Without the PURE annotation, calculateCircumference would still be called in case it had side effects.

Parcel 中文网 - 粤ICP备13048890号