Webpack 5 带来的不只有模块联邦和更快的持久缓存,还内置了一项容易被忽视的声明机制——Theoretical Rights,中文可译为理论权利。它允许开发者在模块级别向打包器承诺该模块在运行时理论上只会触达哪些能力边界,例如是否可能修改全局对象、发起网络请求或者访问浏览器存储。这种承诺一旦成立,打包阶段就可以基于能力标签进行更精确的死代码消除和代码合并。过去很多场景下,打包器因为无法排除某些动态行为而选择全量保留代码,理论权利正是为了解决这类过度保守的问题而设计。

一、设计背景:JavaScript 动态特性带来的打包困境
JavaScript 的灵活性让静态分析变得异常困难。一个模块可以在任何地方读取或写入全局对象,可以通过 eval 执行任意代码,也可以借助动态 import 延迟加载依赖。例如下面这段看似简单的代码:
// 模块内部存在全局访问和条件网络请求
if (typeof window !== 'undefined') {
window.__CACHE__ = {};
}
export function loadUser(id) {
return fetch('/api/user/' + id);
}
打包器面对这种代码,只能假设模块可能修改全局对象并可能发起网络请求。因此即使调用方没有使用 loadUser,相关的 fetch 分支和全局赋值逻辑也很难被安全移除。这种保守策略是产物体积居高不下的重要原因之一。
Webpack 5 的 Theoretical Rights 机制正是为此而来。它借鉴了能力安全模型的思想,要求开发者在模块头部以注释或配置形式声明该模块理论上具备哪些能力。当打包器读到这些声明后,就可以基于声明进行更激进的裁剪。比如一个模块声明自己没有 global 权限,那么其中所有对 window 或 globalThis 的写入操作都可以被安全标记为不可达,并在压缩阶段被删除。
二、如何声明和配置 Theoretical Rights
Webpack 5 为 Theoretical Rights 提供了两种声明途径:在 webpack.config.js 中设置全局默认值,以及在单个模块顶部使用特殊注释覆盖默认配置。配置项位于 optimization.theoreticalRights 下,可以设置 enabled 开启该功能,并通过 defaultRights 指定项目级别的默认权限集合。
module.exports = {
mode: 'production',
optimization: {
theoreticalRights: {
enabled: true,
defaultRights: {
global: false,
network: false,
fs: false,
importMutation: false
}
}
}
};
模块级声明则使用注释形式,格式为 /* theoretical-rights: global=true network=false */。这种注释必须出现在模块的顶部,前面不能有任何可执行代码。多个权限项之间用空格分隔,键值对使用等号连接。常见的权限字段包括 global、network、fs、importMutation 和 eval,分别表示修改全局对象、发起网络请求、访问文件系统、修改导入绑定以及执行动态代码的能力。
下面是一个声明模块只拥有网络权限、不触碰全局对象的示例:
/* theoretical-rights: global=false network=true */
export async function loadRemoteData(endpoint) {
const response = await fetch(endpoint);
return response.json();
}
当模块声明与默认配置冲突时,模块级注释的优先级更高。这种设计允许第三方库在发布时自带准确的权限声明,而应用开发者也可以通过默认配置对整个项目的权限基线进行控制。
三、Theoretical Rights 对打包优化的实际影响
启用 Theoretical Rights 后,Webpack 会在摇树优化之前先解析每个模块的权限声明,并将这些元数据附加到模块对象上。后续的 Tree Shaking、ConcatenateModules 等插件会利用这些信息进行决策。如果一个模块声明没有 global 权限,那么所有对 window 或 globalThis 的赋值语句会被标记为 pure 并可能被直接删除;如果一个模块声明没有 network 权限,那么内部的 fetch 调用分支会被视为不可达,并连同其依赖一起被裁剪。
这种优化比传统的 sideEffects 更加细粒度。sideEffects 只能告诉打包器某个模块整体上是否包含副作用,而 Theoretical Rights 把副作用拆解为不同的能力维度。例如一个模块可能包含对全局对象的修改,但完全没有网络请求,使用 sideEffects: false 无法表达这种部分副作用,而理论权利可以精确到 global=true、network=false。在实际项目中,许多工具库只做纯计算,但为了兼容旧环境会内置一些全局 polyfill 逻辑,通过声明这些 polyfill 不涉及 global 之外的能力,往往能额外减少百分之五到百分之十五的产物体积。
下面是一个对比示例。假设同一个工具函数在不同声明下的打包结果:
// 未声明 Theoretical Rights 时,Webpack 保守保留全部代码
export function formatDate(date) {
if (typeof window !== 'undefined') {
return window.Intl.DateTimeFormat('zh-CN').format(date);
}
const { format } = require('util');
return format(date);
}
// 声明 global=false 后,window 分支会被安全移除
/* theoretical-rights: global=false */
export function formatDate(date) {
const { format } = require('util');
return format(date);
}
需要注意的是,上述例子中 window.Intl 实际上属于全局对象访问,声明 global=false 后 Webpack 才会执行删除。如果没有声明,代码会同时保留浏览器和 Node.js 两套实现。
四、常见误区与安全校验
Theoretical Rights 是一把双刃剑。声明不准确会导致严重的运行时错误。例如一个模块声明没有 global 权限,但实际代码中仍然通过 globalThis 修改了环境,打包后这些修改会被删除,最终在生产环境暴露问题时往往难以排查。因此,项目的单元测试和集成测试必须覆盖声明过的模块,确保没有越权行为。
Webpack 5 在开发模式下会输出权限校验警告。当检测到模块声明的权限与实际行为不一致时,控制台会给出类似 Theoretical rights violation: module 'xxx' declared global=false but accessed window 的提示。开发者可以借助这些警告在构建阶段提前修正声明。此外,还可以通过 optimization.theoreticalRights.strict 配置项开启严格模式,一旦发现违规立即终止构建,而不是仅打印警告。
module.exports = {
mode: 'development',
optimization: {
theoreticalRights: {
enabled: true,
strict: true,
defaultRights: {
global: false,
network: false
}
}
}
};
另一个容易忽视的点是 Theoretical Rights 与 concatenateModules 的交互。当多个模块被合并到同一个作用域时,如果其中某个模块声明了较窄的权限,Webpack 可能会把这种限制传播到合并后的整个函数,导致其他原本拥有更高权限的模块行为被意外裁剪。因此在使用作用域提升的同时开启理论权利,建议先对合并后的作用域进行人工审查,或者通过 optimization.concatenateModules: false 暂时禁用合并且对比打包结果。

