李恒道 发表于 2021-10-2 22:38:32

[油猴脚本开发指南]Proxy与defineProperty性能分析

# 前言

先说结论,不要在proxy以及劫持的情况下大量使用同步的强耗时代码,以及不要proxy频繁读取属性的对象。

能用object.definepeoperty的情况下不要使用proxy。

我的测试量应该已经算很大了,延迟其实还是相对个人认为可以接受的。

但是推荐大家依然注意这个问题。以防以后出现卡顿。

因为,你永远不知道写网页的那个脑瘫写的是什么操作

# 简易测试过程

这里我进行了1000万次的循环,分别为无代理,有代理,代理无Reflect函数,代理无捕获函数,以及Object.defineProperty函数代理。

代码如下

```javascript
    var target = {
      a: 0,
    };
    fornum = 10000000;
    console.time("time_normal");
    let result_normal = 0;
    for (let index = 0; index < fornum; index++) {
      result_normal += target.a;
      target.a = index;
    }
    console.timeEnd("time_normal");
    target.a = 0;
    let handle = {
      get: function (target, prop, receiver) {
      return Reflect.get(target, prop);
      },
      set(target, prop, value) {
      return Reflect.set(target, prop, value);
      },
    };
    let targetproxy = new Proxy(target, handle);
    result_normal = 0;
    console.time("time_proxy");
    for (let index = 0; index < fornum; index++) {
      result_normal += targetproxy.a;
      targetproxy.a = index;
    }
    console.timeEnd("time_proxy");

    target.a = 0;
    let handlenoreflect = {
      get: function (target, prop, receiver) {
      return target;
      },
      set(target, prop, value) {
      return target=value;
      },
    };
    let targetnoreflect = new Proxy(target, handlenoreflect);
    result_normal = 0;
    console.time("time_noreflect");
    for (let index = 0; index < fornum; index++) {
      result_normal += targetnoreflect.a;
      targetnoreflect.a = index;
    }
    console.timeEnd("time_noreflect");




    target.a = 0;
    let handlenotrap = {};
    let targetproxynotrap = new Proxy(target, handlenotrap);
    result_normal = 0;

    console.time("time_proxy_notrap");
    for (let index = 0; index < fornum; index++) {
      result_normal += handlenotrap.a;
      handlenotrap.a = index;
    }
    console.timeEnd("time_proxy_notrap");
    let b = 0;

    Object.defineProperty(target, "a", {
      get: function () {
      return b;
      },
      set: function (str) {
      b = str;
      },
    });
    result_normal = 0;
    console.time("time_define");
    for (let index = 0; index < fornum; index++) {
      result_normal += target.a;
      target.a = index;
    }
    console.timeEnd("time_define");
```

# 运行结果

![图片.png](data/attachment/forum/202110/02/223333jfzfdvk7v8vvfy7v.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片.png")

观察运行结果可知一千万次正常为90毫秒,如果使用了Object.defineProperty,毫秒数与未代理相差无几。

中间的三个是经过proxy代理的对象。

如果我们不使用任何的捕获函数,也就是notrap,其执行速度与无代理的执行速度等同(这里将在最后被推翻)

如果使用了捕获函数。

使用Reflect的情况下,执行速度比normal慢了4100毫秒左右,而不使用reflec的情况下,可以降低1000毫秒的延时。

然后我们再尝试反复引用对象下的属性,以及进行计算,这里我给出代码。

```javascript
    for (let index = 0; index < fornum; index++) {
      result_normal += targetproxy.a;
      targetproxy.a = index;
      targetproxy.b.c=targetproxy.a
      targetproxy.b.d=targetproxy.b.c+1;
    }

```

然后运行进行测试

![图片.png](data/attachment/forum/202110/02/233439sna2iqw9njnmw2s5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "图片.png")

这里可以看到极大的拉开了差距

原生以及object.defineproperty几乎没有改变。

而proxy即使是无handle的proxy对象,依然会极大的延迟。

这是因为代理引发的,我们对其属性进行读写每次都要经过proxy。

其速度受到了极大的影响。

所以proxy的影响速度与proxy创建的对象的属性的读取次数有极大的关联。

所以我们可以得出一个结论。

能不用proxy尽量不要使用proxy。

proxy可以使用,但是不建议用于频繁读取属性的对象上,如果没有高频读取属性的对象是可以使用的。

影响的性能较少。

# 补充理论

还有一个问题就是

如果你书写原生js代码,很有可能浏览器对其进行一定程度的优化的,而proxy代理后则很难进行代码计算优化,为了保持计算的正确性。

以上为个人的一点看法,请不要当做实际情况,如果有测试性能差距特别大的情况可以在评论下贴出代码。

大家一起学习分析

# 结语

撒花~

李恒道 发表于 2021-10-2 22:49:10

@cxxjackie
大佬,这样计算应该没问题吧。。。我有点慌
我看别人计算能得出来defineproperty跟proxy对比,proxy能慢30倍的结论
可能这两年浏览器优化操作了?或者我不严谨?

cxxjackie 发表于 2021-10-2 23:07:47

李恒道 发表于 2021-10-2 22:49
@cxxjackie
大佬,这样计算应该没问题吧。。。我有点慌
我看别人计算能得出来defineproperty跟proxy对比, ...

Proxy我是真的不太熟悉,也很少用,所以分析不出什么东西来。从你测试的数据来看,确实可以得出Proxy比defineProperty慢的结论吧,我觉得不能想当然的认为一千万次测试就可以将结果除以一千万,万一Proxy的损耗主要在于第一次呢?我是从其调用方式猜测的,也许Proxy的原理是对原对象做了一次深拷贝,将代理操作放到拷贝后的对象上进行,这样的话只有第一次深拷贝会比较耗时(可能原对象越复杂,耗时越多),之后都是在现成的对象上进行了,应该跟直接操作原对象的效率差不多。当然这些都是猜测,你可以再验证一下。

李恒道 发表于 2021-10-2 23:13:48

cxxjackie 发表于 2021-10-2 23:07
Proxy我是真的不太熟悉,也很少用,所以分析不出什么东西来。从你测试的数据来看,确实可以得出Proxy比de ...
对proxy进行操作的内容也相当于操作原对象的。
感觉应该不太会涉及深拷贝的问题。
问题也不会调试原生代码。
等我有空再研究看看

我测试的数据来看延迟相差实在太低了。
跟别人测试相差30倍速度简直两个结论,卧槽

李恒道 发表于 2021-10-2 23:16:33

cxxjackie 发表于 2021-10-2 23:07
Proxy我是真的不太熟悉,也很少用,所以分析不出什么东西来。从你测试的数据来看,确实可以得出Proxy比de ...

测试了一下,换不同的循环次数得出延迟基本是等比的

cxxjackie 发表于 2021-10-2 23:19:13

李恒道 发表于 2021-10-2 23:16
测试了一下,换不同的循环次数得出延迟基本是等比的

要不试试把对象弄复杂点,劫持操作也多弄几步?

李恒道 发表于 2021-10-2 23:28:37

cxxjackie 发表于 2021-10-2 23:19
要不试试把对象弄复杂点,劫持操作也多弄几步?

大佬牛逼!破案了!

cxxjackie 发表于 2021-10-3 00:17:13

结果上应该可以说明在xhr/fetch上使用Proxy是不太合适的(至少在脚本里不太合适),请求发生的次数较多,很多页面还会自己用一些库去封装xhr,在此基础上用Proxy造成的性能损耗难以估计,definePeoperty还是有其优势所在的。

李恒道 发表于 2021-10-3 00:47:56

cxxjackie 发表于 2021-10-3 00:17
结果上应该可以说明在xhr/fetch上使用Proxy是不太合适的(至少在脚本里不太合适),请求发生的次数较多,很 ...
我跟大佬意见感觉有点相勃
我觉得fetch不适合用proxy,因为有些网站会在其上面进行属性的读写一堆乱七八糟的东西。
而每个xhr通常只设置了几个属性,读写几次就不会再碰属性了,即使包装了主要的性能消耗感觉可能微乎其微?
如果不是特别频繁,属性的读写消耗可能跟promise的消耗差不多感觉
可能每个xhr就慢0.0几s


我下次再做这个的时候统计一下属性读写次数来观察看看,哥哥晚安
主要fetch劫持可以简单的搞定
xhr的返回内容劫持除了proxy我也想不到什么太好的办法了...

cxxjackie 发表于 2021-10-3 10:00:35

李恒道 发表于 2021-10-3 00:47
我跟大佬意见感觉有点相勃
我觉得fetch不适合用proxy,因为有些网站会在其上面进行属性的读写一堆乱七八糟 ...

xhr也可以用defineProperty的,劫持open就行了,我之前应该发过:
const xhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
const xhr = this;
if (arguments == hookUrl) {
    const getter = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'response');
    Object.defineProperty(xhr, 'response', {
      get: () => {
      let result = getter.call(xhr);
      //这里可以修改result
      return result;
      }
    });
}
return xhrOpen.apply(xhr, arguments);
};
之前用__lookupGetter__有兼容性问题,改成Object.getOwnPropertyDescriptor就好了。
页: [1] 2
查看完整版本: [油猴脚本开发指南]Proxy与defineProperty性能分析