一、背景
事情的起因是因为有位同事发现了我们的代码有性能问题,简单来说就是发现一个对象被new了100万次,执行时间达到了2s,直觉告诉我们这里肯定有问题,这个时间理论上是不应该有的,于是便开始了解密之旅。
涉案代码简化一下其实很简单,这里贴出来:
class Parent {
a;
constructor(a = 0) {
this.a = a;
}
}
class Child extends Parent {
b;
constructor(a = 0, b = 0) {
super(a);
this.b = b;
}
}
为了测试这段代码的性能,我们写了一个简单的benchmark函数:
function bench(count) {
(() => {
let start = Date.now();
for (let i = 0; i < count; i += 1) {
new Child(i, i, i);
}
console.log(Date.now() - start);
})();
}
最后执行:
bench(100 * 10000)
在我们的项目里面,这段代码执行时间需要2s左右
二、定位
我们都知道,现在大部分项目都会使用babel来支持低端浏览器,对于这个性能问题同事发现两个现象:
- 如果Child不继承Parent,那性能没有问题,这个benchmark大概时间在30ms左右
- 如果自己写一个简单的继承,那性能也没有问题,这个benchmark大概时间也在30ms左右
function ext(C, P) {
C.prototype = Object.create(P.prototype);
C.prototype.constructor = C;
}
function Parent(a) {
this.a = a;
}
Parent.prototype.xx = function() {};
function Child(a, b) {
Parent.call(this, a);
this.b = b;
}
ext(Child, Parent);
简单版的继承大概就类似如上代码,那问题很明显了,出在babel上面,难道babel转译出来的代码有性能问题?
使用babel在线编译平台,我们看看这段代码最后是怎样的
"use strict";
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
var Base = function Base(a) {
if (a === void 0) {
a = 0;
}
this.a = a;
};
var Child = /*#__PURE__*/function (_Base) {
_inheritsLoose(Child, _Base);
function Child(a, b) {
var _this;
if (a === void 0) {
a = 0;
}
if (b === void 0) {
b = 0;
}
_this = _Base.call(this, a) || this;
_this.b = b;
return _this;
}
return Child;
}(Base);
简单看一下,也没有什么异常的地方,于是我们只好求助万能的google,很快就找到了一篇文章: stackoverflow.com/questions/4…
And it is also faster than babel's implementation! I have created something similar what babel does to extend a class and tested it in jsPerf. My top 5 test run result was very disappointing with
Object.setPrototypeOf: 19% slower, 20 % slower and three times 21% slower.
可以看到,里面有人提出babel实现的继承比较慢,主要是因为Object.setPrototypeOf,甚至还搜到了一些文章说是Object.setPrototypeOf性能杀手
到这里似乎问题就解决了,我们直接验证一下是不是Object.setPrototypeOf导致的就好了,甚至心里有点确信应该就是这个导致的。
三、没那么简单
很快,同事就来反馈,Object.setPrototypeOf似乎对性能没有影响,我们使用编译后的代码运行了一下,时间差不多是在35ms,基本上没啥差距,看来问题没有那么简单。
现在我们大概知道这个问题应该不是一个通用的问题,大概率是和我们自己的项目构建有关系。那我们只能在我们自己项目中构造最小demo,其实一开始就应该在自己项目中构造最小demo来定位问题的,但是因为项目过于复杂,而且有sourcemap,怕麻烦没有直接去看编译后的问题。但是既然问题已经到了这一步,只能去用performance去看自己的代码了。
performance大法的确好用,我们很快就发现性能瓶颈的确是在new对象上,但是似乎我们项目构建的代码和babel在线构建的不一样。
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Base = function Base() {
var a = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
_classCallCheck(this, Base);
this.a = a;
};
var Child = /*#__PURE__*/function (_Base) {
_inherits(Child, _Base);
var _super = _createSuper(Child);
function Child() {
var _this;
var a = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var b = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
_classCallCheck(this, Child);
_this = _super.call(this, a);
_this.b = b;
return _this;
}
return Child;
}(Base);
这一段代码如果运行100万次,时间的确需要2s,感兴趣的读者可以直接复制代码运行试试,并且performance会显示_createSuperInternal这个函数是最耗时的,那这个函数是怎么来的,我们简化一下代码看看
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived), result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
var Child = /*#__PURE__*/function (_Base) {
_inherits(Child, _Base);
var _super = _createSuper(Child);
function Child() {
// ...
_this = _super.call(this, a);
_this.b = b;
return _this;
}
return Child;
}(Base);
可以看到,_createSuperInternal其实就是在调用父类的构造函数,对比一下一开始性能没问题的版本:
var Child = /*#__PURE__*/function (_Base) {
_inheritsLoose(Child, _Base);
function Child(a, b) {
var _this;
// 差异在这里
_this = _Base.call(this, a) || this;
return _this;
}
return Child;
}(Base);
区别就在于继承父类的时候,有问题的版本是用_super.call,没问题的版本是用_Base.call,继续跟踪一下就会发现,性能的根本在这行代码:
result = Reflect.construct(Super, arguments, NewTarget);
关于Reflect可以直接看MDN上面的规范。 现在问题找到了,那么接下来怎么解决呢:
一种解决方案是我们不用继承,自然不会有这些问题,但是这只是在逃避问题。
另外一种解决方案是深入babel,看看到底是谁导致了构建结果的差异。
很快我们就发现,如果把babel-preset配置项里面的loose配置项改为false,那最终就会使用createSuper版本的继承,但是我们自己的项目构建检查了一遍,loose配置项值是true的。那肯定还有别的东西影响了,最大可能就是插件。我们项目构建比较复杂,用了很多工具,不知道用了多少插件,排查起来有点没有头绪。
这时只能使用笨办法:二分法了。我们在最终编译使用的配置文件地方打日志,首先将所有用到的插件全部打印出来,再通过一遍遍注释相关插件再编译,最终我们锁定了疑犯:@babel/plugin-transform-classes。
打开插件官网,我们就发现:
When extending a native class (e.g.,
class extends Array {}), the super class needs to be wrapped. This is needed to workaround two problems:
- Babel transpiles classes using
SuperClass.apply(/* ... */), but native classes aren't callable and thus throw in this case.- Some built-in functions (like
Array) always return a new object. Instead of returning it, Babel should treat it as the newthis.
这完美符合我们的现状,翻到插件的配置项:
Consider migrating to the top level
assumptionswhich offers granular control over variousloosemode deductions Babel has applied.
好家伙,这就是我们配置的babel的loose属性为true,这里为什么没生效,原来他自己需要单独配置,说是在考虑用babel用的配置。
接下来的事情就比较简单了,找到谁用了这个插件,最终发现其实不是我们自己的构建代码使用了这个插件,是storybook使用了,并且不支持配置loose属性。那只能使用storybook的babel配置能力强行移除这个插件或者加上loose属性来解决问题。
四、总结
至此,问题所有都搞清楚了,因为使用了storybook,而它使用了@babel/plugin-transform-classes插件并且loose为false,最终构建的出来的代码是使用了createSuper包裹,底层再使用Reflect来new对象,从而导致性能慢了有100倍。
但是其实线上是没有这个问题的,因为线上没有使用storybook。
重要的不是问题本身,而是解决问题的过程,这个问题的解决也给我们带来了一些反思:
- 遇到性能问题最好的方式还是立即使用performance来定位问题,毕竟是在问题现场定位
- 二分法虽然有时候效率不高,但是其实挺万能的。一直觉得怎么定位bug其实是有学问的,结合经验进行合理的猜测、尝试,再配合常见的定位方式,往往能有不错的效果
- 前端还是需要有更广阔的视野,像这个问题,如果你对构建没有一些了解的话,查起来的确会一头雾水
最后,希望这个问题对大家有帮助,大家可以检查一下自己线上项目是否有类似问题~