Less.js: 版本 3.10.x 使用显着更多的内存并且比 3.9.0 显着慢

创建于 2019-09-12  ·  95评论  ·  资料来源: less/less.js

我们的构建最近开始失败,因为我们在项目的构建过程中并行运行了大约 80 个 Less 构建,并且新版本的 Less.js 使用了太多内存,以至于 Node 崩溃。 我们将崩溃追溯到从 Less.js 从 3.9.0 升级到 3.10.3。

我更改了我们的 Less 脚本以按顺序编译文件(一次构建 2 个文件)并在此过程中对 Node 的内存使用情况进行采样,并得到以下结果:

less graph

Less.js 现在似乎多使用 130% 的内存,并且为我们编译大约需要 100% 的时间。

只是想知道您是否对 Less.js 进行了基准测试,以及是否看到了类似的结果

最有用的评论

好的团队! 我将发布 3.x 版本,稍后,也许下周,发布 4.0。 两者现在都应该有性能修复。 感谢所有帮助调试的人!

所有95条评论

这很奇怪。 你的节点版本是什么?

@PatSmuk360您能否测试并获取 3.10.0 的内存配置文件,看看它是否有所不同?

我们正在运行最新版本的 10 (10.16.3)。

之前的堆快照:

image

之后的堆快照:

image

此外,我在 Node 12.10.0 上进行了尝试,结果似乎更糟,在顺序构建中的某一时刻达到了 587 MB 的内存使用量。

之前的 CPU 配置文件:
CPU-20190916T133934.cpuprofile.zip

image

之后的 CPU 配置文件:
CPU-20190916T134917.cpuprofile.zip

image

@PatSmuk360所以,总而言之,这些版本之间的区别在于代码库已转换为 ES6 语法。 从技术上讲,这不是重大更改,这就是为什么它不是主要版本的原因。

但是……我怀疑一些 Babel 转换如对象/数组传播语法的效率低于更冗长的 ES5 版本。 这就是我问你是否可以测试 3.10.0 的原因,因为最初我导出了一个与 Node 6 及更高版本兼容的转译包,但它破坏了与无法处理类语法

如果我们能准确地找出哪个 ES5 转换表现不佳,理论上我们可以更精细地将这些 Babel 导出设置设置为更高性能的导出。

@PatSmuk360顺便说一句, split这么多时间的

@matthew-dean 那似乎是String.prototype.split 。 如果您在 chrome devtools 中打开配置文件,您可以看到所有数据颜色编码。 我正在尝试更改配置文件以链接到https://cdn.jsdelivr.net/npm/[email protected]/dist/less.cjs.js作为其来源,以便更容易检查瓶颈。 是否有可用于*.cjs.js*.js文件的源映射? https://cdn.jsdelivr.net/npm/[email protected]/dist/less.min.js.map文件似乎将.min文件映射到 ES6 源。 也许我们可以将源映射提供给开发工具,以便我们可以找出转译导致瓶颈的位置。

特定https://github.com/less/less.js/blob/cae5021358a5fca932c32ed071f652403d07def8/lib/less/source-map-output.js#L78 中的这一行似乎有大量的 CPU 时间。 但考虑到它所执行的操作,对我来说似乎并不合适。

我对堆配置文件没有太多经验,但对我来说突出的是(closures), (system), (array), system / Context数量的增加。 试图将其与 cpu 配置文件联系起来,似乎这些对象的增加导致垃圾收集时间的大量增加。

@kevinramharak一般来说,将具有 _n_ 深度的 Less 之类的 AST 转换为 CSS 之类的序列化、扁平化输出树,需要创建大量临时对象。 因此,可能发生的情况是,通过某些转换,它会添加额外的 _x_ 数量的对象。 即使是暂时的,您也可以获得指数效应,其中每个节点创建的对象甚至只是 2-3 倍,乘以节点数乘以它必须展平规则的次数......我可以看到它加起来。 一般来说,我们可能天真地认为 ES6 语法本质上是 ES5 语法的语法糖。 (一般而言,JavaScript 开发人员可能对此感到内疚。)实际上,转换较新的语法可能会创建性能不高的 ES5 模式。 对于 99% 的开发人员来说,这没什么大不了的,因为他们不会每秒迭代该代码数百或数千次。 但这是我对正在发生的事情的猜测,因为没有其他重大变化。

@kevinramharak回复:源代码行 - 这是因为原始的 Less 解析器不跟踪输入的行/列,所以当添加源映射时,它需要将输入分块到行中以弄清楚它应该如何映射到原始来源。 这在 4.x+ 中不会成为问题,但是现在它会在那里花费大量时间是有道理的。

我在浏览器中使用 less.min.js,3.10.3 比我之前使用的 2.7.3 慢两倍。 在 Chrome 和 Firefox 中。

@PatSmuk360你能看看这个分支吗? https://github.com/matthew-dean/less.js/tree/3.11.0

简而言之,Babel 向 ES5 的转译有点糟糕,并且使用了大量 Object.defineProperty 调用来转译类。 我将转译切换到 TypeScript,它具有更清晰的函数原型输出。

这一切都很好,但这样做之后,Less 的浏览器测试将无法运行,因为它使用的是非常古老和过时的 PhantomJS,到目前为止,没有人(包括我在内)能够成功地将测试迁移出 PhantomJS到无头 Chrome 上。

但是,一次一个问题:如果那个分支上的 dist 源没有内存问题,那么也许我们可以解决浏览器测试的问题。

我已经设法将 Less 浏览器测试迁移到 Headless Chrome,但在性能/稳定性方面,我需要用户的大量反馈才能安全合并,因为将转译管道完全从 Babel 更改为 TypeScript。

当前分支可以在这里找到: https :

仍然比 2.7 慢 2 倍。

@alecpl这是很好的信息,但我真的想知道 3.11.0 是否比 3.10 有所改进。

奇怪的是,在理论上,原始的 Less 代码是用 Lebab 转换的,这应该是 Babel 的反面。 这意味着 ES5 -> ES6 -> ES5 应该几乎相同,但显然并非如此。 所以我需要调查(除非其他人有时间,这是受欢迎的支持)ES6->ES5 代码与原始 ES5 代码有何不同。

@alecpl

因此,我运行了各种测试,并花时间在 Headless Chrome 中对 3.11.0 与 3.10.3 与 3.9.0 与 2.7.3 进行了基准测试

我没有发现任何版本的 Less 编译器都变慢的证据,更不用说慢 2 倍了。 由于转译设置,3.10.0 有更多的内存开销可能仍然是真的,也许如果系统空间受限并因此进行更多的内存交换或更多的 GC,它可能会导致速度变慢? 但我无法验证。

您可以测试自己在 3.11.0 分支上运行grunt benchmark 。 他们可能会在单次运行中报告不同的数字,但如果您运行了足够多的次数,您应该会看到次数大致相等。 所以我不知道你从哪里得到你的数据。

@PatSmuk360您是否能够测试 3.11.0 的内存开销?

我不会自己构建您的代码。 我不使用 nodejs。 我只是从我提到的两个不同版本的 dist 文件夹中取出 less.min.js 文件并在我的页面上使用它们。 然后在控制台中,我看到 Less 代码打印的时间。 我的 less 代码使用多个文件,输出文件大约是 100kB 的缩小 css。 这是一个“现实生活”的基准。 我说的是Roundcube代码。

Chrome 比 Firefox 快得多,但两者版本之间的差异是相似的。

@alecpl嗯....对于那个特定的 Less 代码来说,这可能是一个特殊的区别。 的确,基准是任意的。 如果我有时间,我会将这些 Less 文件添加到基准测试中。

此问题已自动标记为过时,因为它最近没有活动。 如果没有进一步的活动发生,它将被关闭。 感谢你的贡献。

只是想发布一个快速更新:我尝试了 3.11.1,它和 3.10.3 一样慢。 我会看看我是否可以制定一个有代表性的基准来测试/分析这个。

我已经升级到 3.11.1 并且现在有相同的内存消耗问题。
通过 webpack 和less-loader构建一个中等规模的项目需要大约 600MB RAM。

我已经采取了导致这个的堆分配时间表;

heap-timeline

某些事情导致Ruleset保持疯狂的巨大分配。


[编辑]
我正在降级到 3.9 并从中获取堆分配时间线。 去看看我是否发现了任何完全不同的东西。

@马修院长
为你找到了一个提示。

在 3.11 中,列出的 RuleSet 保留器之一是 ImportManager。
在 3.9 中_情况并非如此_。

如果一切都由 ImportManager 保持活动状态,并且 ImportManager 是整个编译过程中的单例。 嗯,是; 这会显着增加内存使用量,因为没有任何东西可以被垃圾收集。 甚至没有中间规则集。

@rjgotten嗯……如果一个对象引用了另一个对象,那为什么会阻止 GC? 我的意思是,从技术上讲,所有对象都通过原型链保留对公共 API 节点的引用。 它只会在相反的情况下阻止 GC,即某些对象保留对每个规则集实例的引用。

你好像误会了。

当“A 是 B 的保留者”时,这意味着 A 正在保留对 B 的引用,这会阻止 B 的垃圾收集。 所以当我写 ImportManager 被列为规则集的保留者时,我表达了你也得出的结论:ImportManager 持有对规则集实例的引用,这防止了这些规则集实例的 GC。

@rjgotten哦,是吗? 我想知道这种变化是如何发生的。 极有可能是我的错,或者是 ES6 重构导致了它。 但是,是的,那绝对可以做到! 感谢调查!

好吧,这个激进的想法怎么样。

我创建了一个分支,樱桃采摘除 ES6 转换之外的所有内容。 这并不简单,而且这会破坏所有的工作,但是如果 Babelified / Typescript 的文件不能胜过现有的原生 JS,那么它就是不值得的。

我不知道我们将如何协调 git 历史,但这里是分支 -> https://github.com/less/less.js/tree/release_v3.12.0-RC1。 Nuking 转换是一件大事,所以我认为这个分支会有一些真正可靠的基准测试来比较。

我想知道这种变化是如何发生的。 极有可能是我的错,或者是 ES6 重构导致了它。 但是,是的,那绝对可以做到! 感谢调查!

这当然很奇怪。 从我能够破译的情况来看,ImportManager 以某种方式通过 ES6 转换添加的胶水代码/polyfill 以某种方式结束了规则集的保留器。

我想知道这里是否有一个中途结束的解决方案。 即保留 Node.js 的 ES6 构建,但也有一个转译的浏览器目标。 取消 ES6 转换增加的代码清晰度将是一个巨大的损失。 😢

@rjgotten

取消 ES6 转换增加的代码清晰度将是一个巨大的损失。

它可能没有看起来那么糟糕,原因有二:

  1. 汇总配置仍然存在,如果有人想研究它,可以从提交中提取。
  2. 一段时间以来,我一直在积极开发基于 TypeScript 的 Less 4.0。 我宁愿花时间在这上面而不是追踪这种性能回归。 这是一个彻底的重构,所以无论如何它都不是很接近,但是根据 TS 的性质,不太可能有这些一次性的对象突变来保持引用并阻止 GC。 并且代码更清晰易懂。 所以就是这样。

将所有内容回滚到 ES5 只是为了解决这个问题似乎有点笨手笨脚,但鉴于正在进行重写,这是可以理解的。 不过,我们应该考虑一下我们必须忍受这个决定多久。 正如@rjgotten提到的,我们会失去很多代码的清晰度,如果 v4

有没有经历过这种情况的人愿意分享他们的代码? 如果我们可以使用真实世界的项目进行基准测试,那就太好了,这样我们就可以确定更改是否有帮助。 如果我们能做到这一点,那么我愿意帮助尝试缩小瓶颈所在。

@matthew-dean,顺便提一下,您是否尝试在松散模式下使用 Babel 进行编译,以查看它是否生成了任何看起来更清晰的代码?

@seanCodes我完全

我的一个猜测是,一些解构逻辑有很多对象创建样板。

基本上,有人必须自愿拥有这个问题。

@matthew-dean,顺便提一下,您是否尝试在松散模式下使用 Babel 进行编译,以查看它是否生成了任何看起来更清晰的代码?

@seanCodes代码是使用 TypeScript 编译的,而不是 Babel。 我的想法是逐渐添加 JSDoc 类型以增强类型检查,但是 v4 中的 TS 足以重写,我不确定是否有必要。 因此,有人可以试验 Babel v. TypeScript(以及每个的不同设置),看看 Babel 是否能生成更高性能的代码。 请非常注意这个问题,这是由最初为 Node.js 生成非 ES5 构建引起的: https :

@seanCodes另外,我仍然发现很难验证性能差异。 没有人提出 PR / 步骤来明确地证明性能的差异。 这个线程中有很多轶事,但没有可重现的代码或步骤,我不确定有人如何调查这个。 理想情况下,会有一个 PR 启动分析工具(通过 Chrome 调试器或其他方式)在系统上输出一个数字,对多个测试进行平均。 所以因为到目前为止,这不是 100% 可复制的,就任何人都能够提供复制步骤而言,这就是为什么我个人不想进入那个兔子洞的部分原因。 (换句话说,“我使用了 Chrome 调试器”的反馈不是一组复制步骤。了解它很有用,但对某人调查没有帮助。我们需要知道我们在跟踪什么以及为什么/结果是什么预期的。)

因此,如果人们想使用当前的代码库来提高 Less 的性能,可能需要多名志愿者和一个人来解决这个问题。

就我个人而言,我没有看到 _speed_ 的性能差异太大,但内存消耗的差异令人震惊,并且似乎与导入量有关,我粗略的堆分析似乎表明这也与问题有关。

如果这是正确的,〜你〜任何人都应该能够将一个可行的测试项目拼凑在一起,只要它包含大量导入的文件,每个文件都包含一些规则集,以查看差异。

@rjgotten

如果这是正确的,您应该能够将包含大量导入文件的测试项目拼凑在一起,每个文件都包含一些规则集并查看差异。

“你”在这里很重要。 😉

“你”在这里很重要。 😉

不幸的词选择。 我的意思是一般意义上的——即“任何人”。
我会自己进一步挖掘,但我目前已经有太多的盘子在旋转。

@rjgotten您能否让我更好地了解“大量导入的文件”可能是什么? 100 个单独的文件会这样做还是我们在谈论 1000 个?

另外,您是如何首先注意到这个问题的? 构建是否由于内存消耗而失败,或者您是否碰巧正在查看内存使用情况? 或者您是否注意到您的机器变慢了?

我还没有尝试复制这个,但我仍然希望了解究竟要寻找什么,这样我就不必求助于猜测和检查。

100个左右就可以了。 这大约是我注意到这个问题的现实项目的规模。

当在我们的企业 CI 环境上运行的构建开始失败时,我第一次注意到它。 我们在配置了内存限制的 Docker 容器中运行它们。

@rjgotten我们在 3.11.3 上还有内存问题吗? 我在以前的版本中删除了导入中 AST 的任何缓存(引用),所以如果导入被保留,并且 AST 树被保留在其中,那么这将增加内存使用量,但如果我们删除被保留的树,这能解决吗?

@matthew-dean 是的,该问题在 3.11.3 上仍然存在。

我会尝试创建一个概念证明,但我有很多事情要做。 一旦我得到一些额外的时间,我就把它放在我的待办事项清单上。

@matthew-dean 我想在一个以前失败的相对较大的项目上测试这个。 有什么我需要知道的吗? 我应该使用这个分支https://github.com/less/less.js/tree/release_v3.12.0-RC1吗?

@nfq您可以尝试一下,但该线程中的普遍智慧不是

顺便说一句,我尝试在编译期间检查less对象以试图找到任何持久对象,但找不到任何对象。 🤷‍♂️

我也遇到了性能问题。 对于相同的测试套件:

| 版本 | 时间 |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |

除了此处讨论的 3.9.0 → 3.10.0 之外, v3.11.2似乎性能显着下降。 变更日志中的这一变化似乎很可疑:

3498 删除导入管理器中的树缓存 (#3498)

我也是。

相同构建的时间(全部在节点 10.19.0 上):

  • v3.9:29.9 秒
  • v3.10:76.0 秒
  • v3.12:89.3 秒

@jnail23

你有一个可以证明这些结果的回购吗?

@matthew-dean 我有一个https://github.com/less/less.js/issues/3434#issuecomment -672580467: https :

对不起,@matthew-dean,我没有。 这些结果来自我雇主的产品。

这些结果来自我雇主的产品。

同样的原因我也不能提供任何文件。 😞

@rjgotten @jrail23 @Justineo - 只是好奇,您是否有许多情况在不同的文件中多次导入相同的导入?

就我而言,我们有一个插件,可以注入@import语句并提供一堆自定义函数。 导入的文件会导入项目的其他部分,最终会产生 1000 多个 Less 变量。

@matthew-dean,我们有一个base.less文件,用于导入常见的东西(例如,颜色变量、排版等)。
我们的应用程序的一些快速搜索显示base.less引用(即<strong i="8">@import</strong> (reference) "../../../less/base.less"; )被 66 个其他组件/功能特定的less文件导入,其中一些文件可能还导入其他特定于组件/功能的 less 文件(它们本身可能引用base.less )。
有趣的是,我们显然还有另一个less文件(可能是意外?)将自身作为参考导入。

@rjgotten @jrail23 @Justineo - 只是好奇,您是否有许多情况在不同的文件中多次导入相同的导入?

对我来说,这是一个“是”。 我第一次遇到问题的项目是一个可换肤的解决方案,它使用了一个集中的变量文件,该文件包含在所有其他文件中。 此外,我们使用基于组件的设计,其中我们有 mixin 工厂,可以生产某些类型的按钮、字体图标等,并且其中的一部分也被导入到多个文件中。

基本上; 每个依赖项都设置为严格导入其依赖项,我们依靠 Less 编译器对导入进行重复数据删除和排序,以正确的顺序并确保正确的 CSS 输出。

@rjgotten

我发布并删除了它(因为我以为我已经弄清楚了,但我仍然无法复制它),仍然没有任何步骤可以重现人们报告的内容,包括什么过程。

甚至像这样简单的事情:

在 3.11 中,列出的 RuleSet 保留器之一是 ImportManager。

我找不到任何证据,但我也不知道你是如何确定的。 我对 Chrome DevTools 不够熟悉,不知道如何确定什么被什么保留这样的事情,这是一个足够模糊的话题,谷歌没有帮助我。 比如,人们是否会附加到 Node 进程中? 在浏览器中运行它并设置断点? 步骤是什么?

简而言之,在本期开刊的那一年,没有一篇报道有重现的步骤。 我相信所有这些轶事都意味着什么,但你是如何确定你发现了什么的? 或者你能想到一个可以证明这一点的设置吗? 我想帮助解决它,但我不知道如何重现它。

@Justineo在您的 repo 中,您采取了哪些步骤来确定内存大小或编译时间? 你是用什么测量的?

@Justineo既然你指出了缓存删除(这可能是个坏主意),你能测试这个分支,看看它是否有助于你的构建速度吗? https://github.com/less/less.js/tree/cache-restored

只是为了今天的实验/测试提供一些更新:

Grunt 的shell:test

  • 少 3.9 -- 1.8 秒
  • 小于 3.12(Babel 转译为 ES5)——9.2s
  • Less 3.12 (Babel-transpiled to ES6) -- 3.1s
  • 小于 3.12(TypeScript 转译为 ES5)——3.2s

所以,我认为总而言之,转译的代码总是更慢,我们天真地不这么想。 我有点惊讶,因为转译现在在 JavaScript 世界中是如此“标准”。 有没有其他研究支持这一点?

@Justineo在您的 repo 中,您采取了哪些步骤来确定内存大小或编译时间? 你是用什么测量的?

npm run test将产生总时间。 在其他项目中,我们在切换到 3.12 时遇到了 OOM。

如果我们查看 TypeScript 生成的代码,我们会得到类似的信息:

Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var node_1 = tslib_1.__importDefault(require("./node"));
var variable_1 = tslib_1.__importDefault(require("./variable"));
var property_1 = tslib_1.__importDefault(require("./property"));
var Quoted = /** <strong i="6">@class</strong> */ (function (_super) {
    tslib_1.__extends(Quoted, _super);
    function Quoted(str, content, escaped, index, currentFileInfo) {
        var _this = _super.call(this) || this;
        _this.escaped = (escaped == null) ? true : escaped;
        _this.value = content || '';
        _this.quote = str.charAt(0);
        _this._index = index;
        _this._fileInfo = currentFileInfo;
        _this.variableRegex = /@\{([\w-]+)\}/g;
        _this.propRegex = /\$\{([\w-]+)\}/g;
        _this.allowRoot = escaped;
        return _this;
    }

对比:

var Node = require('./node'),
    Variable = require('./variable'),
    Property = require('./property');

var Quoted = function (str, content, escaped, index, currentFileInfo) {
    this.escaped = (escaped == null) ? true : escaped;
    this.value = content || '';
    this.quote = str.charAt(0);
    this._index = index;
    this._fileInfo = currentFileInfo;
    this.variableRegex = /@\{([\w-]+)\}/g;
    this.propRegex = /\$\{([\w-]+)\}/g;
};

所以我的猜测是,随着时间的推移,所有这些额外的函数定义和调用都会加起来吗? 除非这是一个红鲱鱼,但我不知道除了转译之外还有什么可以归因于此。 我不明白的是为什么 TS 不能生成更接近原始的代码。

更新:

如果我尝试将 3.9 测试与 3.12 中的测试相提并论,那么我基本上会得到 1.2s 与 1.3s。 所以我不再确定这种差异,因为测试已经改变。 它必须针对完全相同的 less 文件运行。

@Justineo @rjgotten我已经推动了一些 TypeScript 的调整,以期进行更高效的构建。 你想建立并尝试那个分支吗? https://github.com/less/less.js/tree/cache-restored

@matthew-dean 👍 谢谢! 我今天晚些时候试试。

我已经测试了cache-restored分支,它比 v3.11.2+ 快得多,与 v.3.1.0~3.11.1 的速度大致相同。

@贾斯蒂尼奥

我已经测试了缓存恢复分支,它比 v3.11.2+ 快得多,与 v.3.1.0~3.11.1 的速度大致相同。

嗯,这很有希望。 让我们从@rjgotten @jrail23和此线程中的其他人那里获得一些反馈。 该缓存删除是在发布此线程之后(3.11.2); 事实上,这是为了消除一些内存开销,但在您多次导入同一个文件的情况下,它肯定会让事情变得更糟。

所以,简而言之,我仍然不确定原始问题或其原因(除了代码转换中发生的事情),我很想知道这个分支是否仍然存在这些问题,但正如我之前提到的,没有明确的重现步骤,所以我不确定。

@matthew-dean,我在尝试使用cache-restored分支时遇到了一些麻烦(我不太清楚需要做什么才能在本地使用它,因为npm link失败了我)。
你能发布一个我可以尝试的金丝雀/预发布版本吗?

@jnail23

我认为这奏效了。 尝试删除 Less 并使用npm i [email protected]+84d40222

@matthew-dean,我刚试过那个版本,但对我来说仍然不好。
对于新的 webpack 构建(无缓存),v3.9 需要 62.4 秒,v3.13.1 需要 121 秒。
对于缓存构建,v3.9 需要 30 秒,v3.13.1 需要 83-87 秒。

@jrmail23你可以尝试删除所有节点模块并安装[email protected]+b2049010

低于 3.9 基准:
Screen Shot 2020-12-05 at 2 36 09 PM

小于 4.0.1-alpha.0:
Screen Shot 2020-12-05 at 1 12 26 PM

少 4.0.1-alpha.2:
Screen Shot 2020-12-05 at 2 35 20 PM

@Justineo @rjgotten你也可以试试吗?

@jrnail23 @Justineo @rjgotten

请注意,这是一个 4.0 版本,因此如果您的代码有以下重大更改,您可能会遇到错误:

  • mixin 调用需要括号(例如.mixin;是不允许的)
  • Less 的默认数学模式现在是括号除法,所以斜线(作为数学)需要放在括号里

因此,虽然我现在更加乐观,但我已经根据最新的基准解决了这个问题,但我还没有弄清楚为什么会发生这个问题。 如果其他人的表现都适用,我可以简要介绍我如何最终缩小问题范围以及我发现了什么。 换句话说,我想我发现问题是/曾经是_哪里_,但不一定是为什么。

https://github.com/less/less.js/issues/3434#issuecomment -672580467 相同的测试套件的结果:

| 版本 | 时间 |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |
| 4.0.1-alpha.2+b2049010 | ~1.6s |

我可以确认,对于我的特定测试套件,性能水平已提高到与 v3.9.0 一样快。 非常感谢马特! 虽然我不确定数学模式的休息变化。 更改此设置可能会导致我们的许多应用程序损坏,因此即使性能问题已解决,我们也可能会卡在 v3.9.0 上。

@贾斯蒂尼奥

虽然我不确定数学模式的休息变化。

您可以在math=always模式下显式编译以获得以前的数学行为。 这只是一个不同的默认值。

问题分解

TL;DR - 注意类模式

问题在于 Babel 和 TypeScript 中的类的转译。 (换句话说,两者在转译后的代码上都存在相同的性能问题,而 Babel 稍微差一些。)现在,几年前,当引入类模式时,我被告知——直到这个问题,相信——那个类JavaScript 中的继承是函数式继承的语法糖。

简而言之,它不是。 _(编辑:嗯......它是,它不是。这取决于你所说的“继承”是什么意思以及你如何定义它,你很快就会看到......即有几种模式可以在 JavaScript 的原型链中创建“继承”,而类只代表一种模式,但该模式与其他模式有些不同,因此 TS / Babel 需要帮助代码使用专门的函数来“模仿”这种模式。)_

较少的代码如下所示:

var Node = function() {
  this.foo = 'bar';
}

var Inherited = function() {
  this.value = 1;
}
Inherited.prototype = new Node();

var myNode = new Inherited();

现在假设您想使用“现代”JS 重新编写它。 事实上,你可以自动化这个过程,我做到了,但无论哪种方式,我都会把它写成相同的,那就是:

class Node {
  constructor() {
    this.foo = 'bar';
  }
}
class Inherited extends Node {
  constructor() {
    super();
    this.value = 1;
  }
}
var myNode = new Inherited();

同样的事情,对吧? 实际上,没有。 第一个将创建一个属性为{ value: 1 } ,在它的原型链上,它有一个{ foo: 'bar' }

第二个,因为它将在InheritedNode上调用构造函数,将创建一个具有类似{ value: 1, foo: 'bar' }结构的对象。

现在,对于 _user_,这真的无关紧要,因为在任何一种情况下,您都可以从myNode访问valuefoo 。 _功能上_,它们的行为似乎相同。

这是我进入纯猜测的地方

从我记得的关于像 V8 这样的 JIT 引擎的文章中,对象结构实际上非常重要。 如果我创建一堆结构,如{ value: 1 }{ value: 2 }{ value: 3 }{ value: 4 } ,V8 会创建该结构的内部静态表示。 您实际上是将结构+数据存储一次,然后再存储 3 次数据。

但是如果我每次都添加不同的属性,比如: { a: 'a', value: 1 }{ b: 'b', value: 2 }{ c: 'c', value: 3 }{ d: 'd', value: 4 } ,那么这些是 4 个不同的结构,有 4 个数据集,即使它们是从同一组原始类创建的。 JS 对象的每个突变都会对数据查找进行去优化,而转换为函数的类模式会导致(可能)更独特的突变。 (老实说,我不知道这是否适用于浏览器中的本机类支持。)

AFAIK,同样正确的是,您在对象上拥有的属性越多,单个属性查找所需的时间就越长。

同样对于最终用户来说,这并不重要,因为 JS 引擎太快了。 Buuuuut 说你正在维护一个引擎,该引擎尽可能快地在很多对象上创建和查找属性/方法。 (叮叮叮。)突然之间,TypeScript / Babel“扩展”对象的方式和原生功能原型继承之间的这些小差异很快就加起来了。

我使用旧的 Less 语法和用 TS 转译的类模式编写了一个 Node 和一个继承函数的基本实现。 刚一开始,继承节点就多消耗 25% 的内存/资源,这是在创建继承节点的任何实例之前。

现在考虑一些节点从其他节点继承。 这意味着,类的内存表示开始成倍增加,它们继承的实例也是如此。

再次,这是猜测

我必须强调对这一切持保留态度,因为我从未听说过转换为类对性能来说是灾难性的。 我_认为_正在发生的事情是,Less 正在使用一些特殊的对象/实例查找组合,这严重降低了 JIT 的优化。 _(如果一些 JavaScript 引擎专家知道为什么在这种情况下转译类的性能比原生 JS 继承方法差这么多,我很想知道。)_ 我试图创建一个使用这两种方法创建数千个继承对象的性能度量,并且我永远无法看到性能的一致差异。

因此,在您离开“JavaScript 中的类很糟糕”之前,Less 代码库中可能还有其他一些非常不幸的模式,再加上转译类与原生 JS 之间的差异,导致了这种性能下降。

我是如何最终弄明白的

老实说,我从来没有怀疑过阶级模式。 我知道原始 ES5 代码比反向 Babelified 代码运行得更快,但我怀疑箭头函数或散布语法的某些地方。 我仍然拥有最新的 ES5 分支,所以有一天我决定再次运行lebab ,并且只使用这些转换: let,class,commonjs,template 。 那是我发现它再次出现性能问题的时候。 我知道这不是字符串模板或 let-to-var; 我想也许需要导入做了一些事情,所以我玩了一段时间。 就这样离开了课堂。 因此,我凭直觉将所有扩展类重写为函数式继承。 砰,表演又回来了。

一个警示故事

所以! 学过的知识。 如果您的项目使用旧的 ES5 代码,并且您渴望“现代” Babel 化或 TypeScripted 的优点,请记住,无论您转译什么,都不是您编写的。

我仍然能够将类重写为更“类”的东西,具有稍微更易于维护的模式,但或多或​​少保留了 Less 节点的原始功能继承模式,我将在项目中留下一个强烈的注释未来的维护者永远不会再这样做了。

您可以在 math=always 模式下显式编译以获得以前的数学行为。 这只是一个不同的默认值。

我知道这一点。 我们在许多不同的团队中有许多 Less 代码库,因此即使在默认编译选项中中断更改也会增加通信成本。 我不确定这些好处是否值得破坏。

谢谢详细的分解!

我在编译后的输出中看到了Object.assign的用法,这意味着它只适用于支持原生 ES class语法的浏览器,除非现在需要 polyfills。 那么,如果我们打算放弃对旧环境(例如 IE11、Node 4 等)的支持,我们是否可以只使用本机语法而不转换为 ES5?

同时,我认为最好能将性能下降的修复和破坏性更改分开,这意味着在 v3 中实现性能修复并仅在 v4 中包含破坏性更改。

@马修院长
ES 类是罪魁祸首的事实非常可怕。
感谢您的详细分解。

去展示:_composition over继承_ 😛

虽然仅供参考; 如果你想要一个更清晰的原型继承链,你真的应该和你的例子做一些不同的事情。
如果您像这样使用Object.create

var Node = function() {
  this.foo = 'bar';
}
Node.prototype = Object.create();
Node.prototype.constructor = Node;

var Inherited = function() {
  Node.prototype.constructor.call( this );
  this.value = 1;
}
Inherited.prototype = Object.create( Node.prototype );
Inherited.prototype.constructor = Inherited;

var myNode = new Inherited();

然后将属性展平到实例本身,然后共享相同的形状; 原型共享形状; _并且_您不必为每个属性访问爬上原型链。 😉

@贾斯蒂尼奥

我在编译输出中看到了 Object.assign 的用法,这意味着它只适用于支持原生 ES 类语法的浏览器,除非现在需要 polyfills。 那么,如果我们打算放弃对旧环境(例如 IE11、Node 4 等)的支持,我们是否可以只使用本机语法而不转换为 ES5?

我认为这不太正确,即 Object.assign 在类实现之前不久登陆,但您的观点已被采纳。 我只是希望避免一遍又一遍地写[Something].prototype.property :/

从技术上讲,一切仍在进行中,所以我不知道。 最初的目标是一个更易于维护/可读的代码库。 如果您在某些环境中需要 Object.assign polyfill,那就这样吧。 它将在 Less 不支持的某个版本上。

同时,我认为最好能将性能下降的修复和破坏性更改分开,这意味着在 v3 中实现性能修复并仅在 v4 中包含破坏性更改。

我一直在考虑这一点,我想这也是一个公平的观点。 我只是想让我的头脑不爆炸构建/维护 Less 的 3 个主要版本。

@rjgotten AFAIK 这就是类模式应该做的事情,正如您定义的那样。 除非我没有看到关键差异。 所以🤷‍♂️ . 如果它运行良好,我不会尝试再次更改 Less 的节点继承模式(除了我可能会按照@Justineo 的建议删除此Object.assign调用。)

@Justineo @rjgotten你能试试这个吗: [email protected]+b1390a54

刚试了一下:

| 版本 | 时间 |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |
| 4.0.1-alpha.2+b2049010 | ~1.6s |
| 3.13.0-alpha.10+b1390a54 | ~4.7s |

附言。 使用 Node.js v12.13.1 测试。

什么即时通讯。

没有时间设置单独的基准。
但是从集成测试的角度来看,这里有一些真实世界的数字:使用 Less 的生产级 Webpack 项目的累积结果。

版本 | 时间 | 峰值内存
:------------------------|--------:|------------:
3.9 | 35376ms | 950MB
3.11.3 | 37878ms | 920MB
3.13.0-alpha.10+b1390a54 | 34801ms | 740MB
3.13.1-alpha.1+84d40222 | 37367ms | 990MB
4.0.1-alpha.2+b2049010 | 35857ms | 770MB

对我来说,3.13.0 给出了_best_ 的结果...... 🙈

3.11.3 似乎也没有我之前在 3.11.1 版本中看到的失控内存使用。
但是 4.0.1 和 3.13.0 测试版仍然更好。

恢复缓存的 3.13.1 无助于改善我的实际编译时间,只会增加内存使用量。

@rjgotten是的,我认为问题在于,在这个线程中,人们衡量的是不同的东西,到目前为止,每个人基本上都有无法复制的私人数据(无需提交较少的基准测试,或将其转换为 PR)。 Less 确实有一个基准测试,我可以用它来针对 repo 的不同克隆来验证自己在解析/评估时间上的差异,但是缓存不(当前)适用的地方。

这是一个已发布的构建,在树上进行了继承更改,并恢复了解析树缓存: [email protected]+e8d05c61

测试用例

https://github.com/ecomfe/dls-tooling/tree/master/packages/less-plugin-dls

结果

| 版本 | 时间 |
| -- | -- |
| v3.9.0 | ~1.6s |
| v3.10.0~v3.11.1 | ~3.6s |
| v3.11.2+ | ~12s |
| 4.0.1-alpha.2| ~1.6s |
| 3.13.0-alpha.10| ~4.7s |
| 3.13.0-alpha.12 | ~1.6s |

Node.js 版本

v12.13.1


对我来说,这个版本似乎工作得很好。 我会请我的同事尝试并验证。

对我来说比alpha.10稍差,但也可能只是差异:

版本 | 时间 | 峰值内存
:------------------------|--------:|------------:
3.9 | 35376ms | 950MB
3.11.3 | 37878ms | 920MB
3.13.0-alpha.10+b1390a54 | 34801ms | 740MB
3.13.0-alpha.12+e8d05c61 | 36263ms | 760MB
3.13.1-alpha.1+84d40222 | 37367ms | 990MB
4.0.1-alpha.2+b2049010 | 35857ms | 770MB

@matthew-dean,看起来我的代码与4.0.1-alpha.2+b2049010更改不兼容。 我会看看我是否可以解决我的问题并尝试一下。

@rjgotten是的,对于您的测试用例,差异似乎很小。

我认为这样做的结果是我可能会同时准备 3.x 和 4.0 版本。 我不想在这个问题未解决的情况下推动 4.0。 值得庆幸的是,它看起来已经如此。

我要做的另一项是为任何人打开一个问题,即放入一个 CI 管道,如果它低于某个基准阈值,则该管道将失败。

@jrnail23如果您可以修改以运行 4.0 alpha,那就太好了,但您可以同时尝试[email protected]+e8d05c61吗?

@matthew-dean,我的构建得到以下内容:

  • v3.9:98 秒
  • v3.10.3:161 秒
  • v3.13.0-alpha.12:93-96 秒

所以看起来你又回到了你想去的地方! 干得好!

干得好!

是的,我要第二; 第三; 第四点。
这是一个令人讨厌的、令人讨厌的性能回归,确实需要花费大量的精力来清理。

工作_非常好_完成。

好的团队! 我将发布 3.x 版本,稍后,也许下周,发布 4.0。 两者现在都应该有性能修复。 感谢所有帮助调试的人!

顺便说一句,我目前正在将 Less.js 代码库转换为 TypeScript,并计划利用这个机会进行一些性能调整/重构。 如果有人感兴趣,我愿意提供帮助! https://github.com/matthew-dean/less.js/compare/master...matthew-dean :next

有什么特别的地方你更喜欢多一些眼睛吗? PR很大,不知道从何说起

@kevinramharak这是一个公平的问题。 老实说,我在转换为 TypeScript 时遇到了意想不到的障碍(变得难以解决的错误),现在我正在重新考虑这样做。 Less 运行良好,而且我想要转换的很多原因(使重构/添加新语言功能更容易)现在都无关紧要,因为我决定将我对新语言功能的想法转移到新的预处理语言。 我不想过度自我宣传,所以如果你想了解详情,请在 Twitter 或 Gitter 上私信我。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

awebdev picture awebdev  ·  4评论

Oskariok picture Oskariok  ·  6评论

heavyk picture heavyk  ·  3评论

BrianMulhall picture BrianMulhall  ·  4评论

rejas picture rejas  ·  6评论