作用域提升
从历史上看,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 还静态分析每个模块的导入和导出,并删除所有未使用的内容。这称为 "摇树" 或 "死代码消除"。静态和 动态导入、CommonJS 和 ES 模块 都支持 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.
编译为类似:
¥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 模块 import
和 export
语句、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.
// ✅ 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 模块内的 exports
、module.exports
和 this
进行静态分配。这意味着属性名称必须在构建时静态已知(即不是变量)。
¥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;
避免 module
和 exports
重新分配
#¥Avoid module
and exports
re-assignment
当 CommonJS module
或 exports
变量被重新分配时,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:
-
false
- 该包中的所有文件都没有副作用。¥
false
– All files in this package have no side effects. -
string
- 包含副作用的全局匹配文件。¥
string
– A glob matching files that includes side effects. -
Array<string>
- 包含副作用的 glob 匹配文件数组。¥
Array<string>
– An array of globs matching files that include side effects.
当文件被标记为无副作用时,如果 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.
在本例中,仅使用 math
库中的 add
函数。multiply
和 elapsed
未使用。通常,仍然需要 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.