加速JavaScript - 失控的Polyfills

在之前的帖子中,我们关注了运行时性能,我认为现在看看 Node 模块的安装时间也很有趣。关于各种算法优化或使用更高效的系统调用已经有很多文章,但我们为什么一开始就有这个问题呢?为什么每个 node_modules 文件夹都这么大?这些依赖都是从哪里来的?

一切都始于我的一个朋友注意到他的项目的依赖树有点奇怪。每当他更新一个依赖项时,它都会引入几个新的依赖项,随着每次后续更新,总依赖数量逐渐增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
├─┬ arraybuffer.prototype.slice 1.0.2
│ └─┬ define-properties 1.2.1
│ └── define-data-property 1.1.0
├─┬ function.prototype.name 1.1.6
│ └─┬ define-properties 1.2.1
│ └── define-data-property 1.1.0
├─┬ globalthis 1.0.3
│ └─┬ define-properties 1.2.1
│ └── define-data-property 1.1.0
├─┬ object.assign 4.1.4
│ └─┬ define-properties 1.2.1
│ └── define-data-property 1.1.0
├─┬ regexp.prototype.flags 1.5.1
│ ├─┬ define-properties 1.2.1
│ │ └── define-data-property 1.1.0
│ └─┬ set-function-name 2.0.1
│ └── define-data-property 1.1.0
├─┬ string.prototype.trim 1.2.8
│ └─┬ define-properties 1.2.1
│ └── define-data-property 1.1.0
├─┬ string.prototype.trimend 1.0.7
│ └─┬ define-properties 1.2.1
│ └── define-data-property 1.1.0
└─┬ string.prototype.trimstart 1.0.7
└─┬ define-properties 1.2.1
└── define-data-property 1.1.0

公平地说,一个包可能依赖于其他附加的依赖项是有合理理由的。然而,在这里,我们开始注意到一个模式:新的依赖项都是为 JavaScript 函数提供的 polyfill,而这些函数在所有地方早已得到支持。例如,Object.defineProperties 方法是作为 Node 0.10.0 的第一个公共版本的一部分发布的,可以追溯到2013年。甚至连 Internet Explorer 9 都支持它。然而,许多包都依赖于它的 polyfill

在引入 define-properties 的各种包中,有一个引起了我的注意,那就是 eslint-plugin-react。它引入了 Object.defineProperties polyfill,但没有 JavaScript 引擎是没有内置它的。

Polyfills是无效的

阅读这些包的源代码揭示了更奇怪的事情:这些 polyfill 函数是直接导入和调用的,而不是在运行时环境中补丁缺失的功能。polyfill 的整个目的是对用户的代码是透明的。它应该检查要补丁的函数或方法是否可用,并仅在缺失时添加它。当不需要 polyfill 时,它应该什么都不做。对我来说奇怪的是,这些函数被直接使用,就像是库中的函数。

1
2
3
4
5
6
7
8
9
10
// Why is the `define` function imported directly?
var define = require("define-properties");
// ...

// and even worse, why is called directly?
define(polyfill, {
getPolyfill: getPolyfill,
implementation: implementation,
shim: shim,
});

相反,它们应该直接调用 Object.definePropertiespolyfill 的整个目的是对环境进行补丁,而不是直接调用。将其与 Object.defineProperties polyfill 应该是什么样子进行比较:

1
2
3
4
5
// Check if the current environment already supports
// `Object.defineProperties`. If it does, then we do nothing.
if (!Object.defineProperties) {
// Patch in Object.defineProperties here
}

最常见的使用 define-properties 的地方讽刺的是在其他polyfill中,而这些 polyfill 又加载了更多的 polyfill。在你问之前,define-properties 包依赖的不仅仅是它自己。

1
2
3
4
5
6
7
8
9
10
var keys = require("object-keys");
// ...
var defineDataProperty = require("define-data-property");
var supportsDescriptors = require("has-property-descriptors")();

var defineProperties = function (object, map) {
// ...
};

module.exports = defineProperties;

eslint-plugin-react 内,通过对 Object.entries() polyfill 进行加载以处理它们的配置:

1
2
3
4
5
6
7
8
9
const fromEntries = require("object.fromentries"); // <- Why is this used directly?
const entries = require("object.entries"); // <- Why is this used directly?
const allRules = require("../lib/rules");

function filterRules(rules, predicate) {
return fromEntries(entries(rules).filter(entry => predicate(entry[1])));
}

const activeRules = filterRules(allRules, rule => !rule.meta.deprecated);

本地清理

在撰写本文时,安装eslint-plugin-react总共引入了令人惊讶的 97 个依赖项。我对其中有多少是 polyfill 感到好奇,并开始在本地逐个补丁。所有事情做完后,这将总依赖数量减少到了 15。原始的 97 个依赖项中有 82 个是不需要的。

巧合的是,同样在各种 eslint 预设中很受欢迎的 eslint-plugin-import 也存在类似的问题。安装该插件会填满您的 node_modules 文件夹,共有 87 个包。在经过另一轮本地清理后,我成功将这个数字缩减到了 17。

空间整理

现在您可能想知道您是否受到影响。我进行了快速搜索,基本上您能想到的每个广受欢迎的 eslint 插件或预设都受到了影响。由于某种原因,整个事件让我想起了一段时间前整个行业发生的 is-even/is-odd 事件。

拥有如此多的依赖项使得审核项目的依赖项变得更加困难。这也是对空间的浪费。举例来说:仅删除项目中的所有 eslint 插件和预设就减少了 220 个包。

1
2
3
pnpm -r rm eslint-plugin-react eslint-plugin-import eslint-import-resolver-typescript eslint-config-next eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-prettier prettier eslint-config-prettier eslint-plugin-react-hooks
Scope: all 8 workspace projects
. | -220 ----------------------

我只是想要一些代码规范检查的规则。我不想要一堆我不需要的 polyfill