likes
comments
collection
share

(译)Electron的remote模块可能是有害的

作者站长头像
站长
· 阅读数 31

原文:nornagon.medium.com/electrons-r…

平时在Electron中写 remote 的时候,一股脑就用了,也没有深入探究 remote 会带来的性能影响,刚好同事推荐了这篇文章,就研究研究浅浅翻译了一下,有啥问题欢迎交流指正

从最早的 Electron 版本开始,remote 模块就一直是在主进程和渲染进程之间进行通信的首选工具。基本的前提是:从渲染进程中,向 remote 模块请求主进程中对象的句柄。然后,你就可以像使用渲染进程中的普通 JavaScript 对象一样使用该句柄——调用方法、等待 promises 和注册事件处理程序。渲染器和主进程之间的所有 IPC 调用都在幕后为您处理,非常方便!

然而,许多重要的Electron应用(如Slack)在采用remote模块后,最终都对其决定感到后悔。为什么?

注:Slack 是一个用于团队协作的聊天和通信工具,为 Electron 框架的代表性作品

特点分析

1.它的运行速度很慢

Electron 以 Chromium 为基础,继承了 Chromium 的多进程模型。有一个或几个渲染器进程,负责渲染 HTML/CSS 和在页面上下文中运行 JS,还有一个主进程,负责协调所有渲染器并代表它们执行某些操作。

当渲染进程访问远程对象时,例如读取属性或调用函数,渲染进程会向主进程发送消息,要求其执行该操作,然后阻塞等待响应。这意味着当渲染器在等待结果时,它什么也不能做,只能无所事事。不解析传入的网络数据,不渲染,不处理计时器。只是在等待。

在我的机器上,访问远程对象上的属性的平均时间约为 0.1 ms。相比之下,访问渲染器本地对象上的属性大约需要 0.00001 ms GitHub。远程对象比本地对象慢一万倍。我用大字体写一下,因为这很重要。

远程对象比本地对象慢一万倍。

每隔一段时间进行一两次 0.1 毫秒的调用并不是什么大问题——如果你想保持在一帧内,0.1 毫秒与 16 毫秒相比仍然是相当快的。假设你没有做其他事情,那么每帧调用远程对象的预算是 160 次。

但是很容易不小心进行比预期更多的远程调用。例如,下面的代码想象了一个自定义域对象,它存在于主进程中,并通过渲染器进行操作:

// Main process
global.thing = {
  rectangle: {
    getBounds() { return { x: 0, y: 0, width: 100, height: 100 } }
    setBounds(bounds) { /* ... */ }
  }
}
// Renderer process
const thing = remote.getGlobal('thing')
const { x, y, width, height } = thing.rectangle.getBounds()
thing.rectangle.setBounds({ x, y, width, height: height + 100 })

在渲染进程中执行这段代码涉及 9 个往返 IPC 消息:

  1. 初始的 getGlobal()调用,它返回一个代理对象;
  2. 从 thing 获取矩形属性,它返回另一个代理对象,
  3. 在矩形上调用 getBounds(),它返回第三个代理对象,
  4. 求出边界的 x 属性,
  5. 得到积分限的 y 属性,
  6. 得到边界的 width 属性,
  7. 得到边界的 height 属性,
  8. 再次获取事物的矩形属性,它返回与(2)中相同的代理对象,
  9. 用新值调用 setBounds。

这三行代码——不是一个循环!-执行几乎需要整整一毫秒。一毫秒是很长的一段时间

当然可以优化此代码以减少完成此特定任务所需的 IPC 消息的数量(实际上,还需要一些特殊的内部 Electron 数据结构,如从 BrowserWindow 返回的 bounds 对象。getBounds 具有神奇的属性,使它们更高效)。但是,像这样的代码很容易进入应用程序尘封的角落,并最终产生“千刀斩乱麻”的效果——在检查时看起来不起眼的代码实际上比看起来慢很多。如果这些代理对象是从创建它们的函数返回的,那么它们可能会出现在各种地方,这使得这个问题变得更加复杂,导致那些缓慢的远程ipc在距离最初调用 remote.getGlobal()很远的地方被调用。

2.它会造成潜在的时效问题

我们通常认为 JavaScript 是单线程的(Node 中新的工作线程模块除外)。也就是说,当代码运行时,不会发生其他事情。这在 Electron 中仍然是正确的,但是当使用远程模块时,有一些微妙的技巧可能导致您不希望存在的竞态条件。例如,考虑这个相对常见的 JavaScript 模式:

注:如果程序运行顺序的改变会影响最终结果,这就是一个竞态条件(race condition)

obj.doThing()
obj.on('thing-is-done', () => {
  doNextThing()
})

在这里 doThing 会启动一些最终会触发thing-is-done事件的过程。Node 中的 http 模块就是一个很好的例子,它通常以这种方式使用。这在普通 JavaScript 中是安全的,因为在代码完成运行之前没有办法触发 thing-is-done 事件。

但是,如果 obj 是远程对象的代理,则此代码包含竞争条件。doThing 是一个可以很快完成的操作。当我们在渲染进程中的代理对象上调用 obj.doThing()时,remote 模块会向底层的主进程发送一个 IPC。然后在主进程中调用 doThing(),它启动它所做的任何事情,将 undefined 作为返回值返回给渲染器进程。现在有两个线程在执行:正在做事情的主进程和即将向主进程发送消息请求为obj添加事件处理程序的渲染进程。如果事情完成得特别快,则可能会发生在消息到达之前,在主进程中触发thing-is-done事件,通知主进程渲染器进程对该事件感兴趣。

主进程和渲染进程之间的竞争条件导致意外行为:

(译)Electron的remote模块可能是有害的

这里的主进程和渲染进程都是单线程的普通 JavaScript。但它们之间的交互会导致竞态条件,在调用 doThing()和调用 on('thing-is-done')之间触发事件。

如果这看起来令人困惑和微妙,那是因为事实如此。Electron 自己的测试套件包含了这种竞态条件的许多不同版本,直到最近一项减少测试缺陷的努力发现了他们。

3.远程对象和常规对象有细微不同

当您从remote模块请求一个对象时,您将得到一个代理对象——它代表另一端的实际对象。remote模块尽其所能让这个对象看起来就像它真的存在于当前进程中一样,它做得很好,但是有很多奇怪的边界情况,使得远程对象以不同的方式在前99次运行正常,但在第100次时就会以某种极其难以调试的方式失败。下面是一些例子:

  1. 原型链在进程之间没有镜像。 因此,例如 remote. getGlobal ('foo').constructor.name === "Proxy",而不是远程端构造函数的真实名称。任何涉及原型的稍微聪明的东西,如果接触到远程对象,肯定会爆炸。
  2. remote模块无法正确处理NaN和Infinity。如果远程函数返回NaN,则渲染程序进程中的代理将返回undefined。
  3. 在渲染进程中运行的回调函数的返回值不会传递回主进程。当你将函数作为回调函数传递给远程方法时,无论渲染程序进程中的方法返回什么,从主进程调用该回调函数将始终返回undefined。这是因为主进程不能阻塞等待渲染进程返回结果。

当您第一次使用remote模块时,很可能不会遇到任何这些细微的差异。甚至可能是第一百次。但是,当您意识到remote模块工作的小问题导致您花费六小时试图找出bug时,要轻易改变使用remote的决定就太晚了。

4.存在即将出现的安全漏洞

很多 Electron 应用程序从来没有故意运行不受信任的代码。然而,在应用程序中启用沙箱仍然是一个明智的预防措施——例如,显示任意用户控制的图像是很常见的,而且这种情况也不是没有听说过,比如PNG解码中包含bug

但是沙盒渲染器的安全性取决于主进程。渲染器与主进程通信,请求代表它执行的操作——例如,打开一个新窗口或保存一个文件。当主进程接收到这样的请求时,它会决定是否允许渲染程序执行该操作,如果不允许,它将忽略该请求并因不良行为而随意关闭渲染器进程。(或者直接拒绝请求,这取决于违规的严重程度。)这里有一个明确的安全边界:无论渲染进程请求什么,主进程都负责决定是否允许它。

remote模块在安全边界上撕开了一个卡车大小的大洞。如果渲染进程可以向主进程发送请求,说“请获取这个全局变量并调用此方法”,那么被妥协的渲染进程就有可能制定并发送一个请求,要求主进程做任何它想做的事情。实际上,remote 模块使沙盒几乎毫无用处。Electron 提供了一个禁用远程模块的选项,如果你在应用中使用沙盒,你肯定也应该禁用远程。

remote代表的安全漏洞的大小。:

(译)Electron的remote模块可能是有害的

我甚至还没有触及一个主要的问题:remote 实现的固有复杂性。在进程之间桥接 JS 对象可不是件小事:例如,远程必须在进程之间传播引用计数,以防止对象在另一个进程中被GC。这项任务非常具有挑战性,如果没有大量的记录和管理以及精细的 C++ 代码,它是无法完成的(尽管一旦WeakRefs可用,纯 JavaScript 也可以做到)。即使有了这些机制,remote 也不能(很可能永远不能)正确地进行 GC 循环引用。世界上很少有人完全了解 remote 的实现,修复其中发生的错误是非常困难的。

注:weakref指弱引用,是一种可以避免内存泄露的编程技术

remote 模块运行缓慢,容易出现条件竞争,产生的对象与普通 JS 对象略有不同,并且存在巨大的安全隐患。不要在你的应用中使用它。

我们应该怎么办?

为了优化性能,理想情况下,我们应该在应用程序中减少IPC的使用,并将更多的工作分配给渲染进程。如果需要在同源的多个窗口之间通信,可以使用 window.open()并同步编写脚本,就像在 Web 上一样。对于不同来源的窗口之间的通信,有 postMessage。

但是当你真正需要在主进程中调用一个函数时,我建议你使用 Electron 7 提供的新方法ipcRenderer.invoke()。它的工作原理与功能强大的 ipcRenderer.sendSync()类似,但它是异步的,也就是说它不会阻塞渲染器中发生的其他事情。下面的例子将一个用于加载文件的远程系统转换为一个基于 ipcRenderer .invoke()的系统:

之前,使用 remote:

 // Main 
    global.api = {
      loadFile(path, cb) {
        if (!pathIsOK(path)) return cb("forbidden", null)
        fs.readFile(path, cb)
      }
    }
    // Renderer
    const api = remote.getGlobal('api')
    api.loadFile('/path/to/file', (err, data) => {
      // ... do something with data ...
    })

之后,用 invoke:

 // Main
    ipcMain.handle('read-file', async (event, path) => {
      if (!pathIsOK(path)) throw new Error('forbidden')
      const buf = await fs.promises.readFile(path)
      return buf
    })
    // Renderer
    const data = await ipcRenderer.invoke('read-file', '/path/to/file')
    // ... do something with data ...

或者使用 ipcRenderer.send (Electron6 或更老版本):

注意,这种方法一次只能处理一个未处理的请求,除非你做了一些记录来跟踪哪个响应属于哪个请求。(invoke()自动处理与请求匹配的响应。)

 // Main
    ipcMain.on('read-file', async (event, path) => {
      if (!pathIsOK(path))
        return event.sender.send('read-file-complete', 'forbidden')
      const buf = await fs.promises.readFile(path)
      event.sender.send('read-file-complete', null, buf)
    })
    // Renderer
    ipcRenderer.send('read-file', '/path/to/file')
    ipcRenderer.on('read-file-complete', (event, err, data) => {
      // ... do something with data ...
    })
    // Note that only one request can be made at a time, or else
    // the responses might get confused.

这是一个小例子,使用 IPC 时你需要做的事情可能更复杂,同时也没法干净利落直接转换为invoke。 但是,采用这种方式编写IPC处理程序会使您的应用程序更加清晰、易于调试、健壮和安全。

转载自:https://juejin.cn/post/7387250487622664201
评论
请登录