Electron (类似的还有 nw.js)是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入了 Chromium 和 Node.js。

也就是说,我们几乎可以使用纯 web 技术,来创建跨平台的 windows 和 macOS 的原生应用,并通过 Node.js addon 能力接入 native 模块。目前市面上,也有一大批知名的应用是使用 Electron 开发的,比如:VS Code、Atom、Microsoft Teams 等(在 macOS 上面一个简单的判断应用是否使用了 Electron 的办法:在应用的 Contents/Frameworks 里面搜索是否有 Electron Framework.framework)。

但实际上,这篇文章是希望你在选用 Electron 框架前,需要进行慎重的考虑和评估。国内有很多公司,包括一些一线互联网公司的项目是一开始为了快速迭代选择了 Electron,后续实在无法进一步优化,全部推到重来,这实际上反而不利于整体的项目迭代。

架构选型

一般来说,笔者认为有以下几个场景,不适合使用 Electron 进行开发:

1. 无页面或者少量页面的应用

这一点很好理解,Electron 的便利性主要体现在页面相关的开发,如果你的应用几乎没有页面,比如只在顶部状态栏区域有一个按钮,显然就没有必要使用 Electron,直接使用原生的技术栈即可。

2. 对安装包体积限制较为严格的应用

Electron 由于自身携带的基础设施,导致即使你的业务代码不多,初始安装包也会比较大(毕竟接近一个浏览器的大小),在没有你的业务代码的情况下,未经优化的安装包达到了 60MB 左右,而且通常你需要把 node_modules 一起打进去,所以即使你的业务并不复杂,也很容易产生一个接近 100MB 的安装包。

因此,如果你的业务需要比较极致的包体积优化,那么 Electron 可能并不是一个合适的选择。

3. 多窗口应用

Electron 的进程模型为一个主进程 + 若干渲染进程,每一个渲染进程用于展示一个页面,即使你的页面是 Hello World,内存占用也达到 50 MB 左右

也就是说,如果你的应用需要同时展示多个窗口,那么就需要多个渲染进程,这样整体的内存占用就会上涨很多,而实际上我们使用原生或者其他的类 cef 的方案,是可以做到一个进程对应多个窗口的。

4. 性能消耗较高并且需要高度定制优化的应用:比如视频类应用

Electron 基于 web 架构,所以使用 Electron 开发的应用性能一般来说和 web 比较接近,当然,我们可以通过 Node.js addon 加持的方式让部分场景下性能更高(比如直接使用 c++ 实现一些计算密集型的模块,或者独立出一个非 UI 进程,来处理非 UI 逻辑),不过页面 UI 相关的还是会受限制于 web 的天花板。

所以,一般来说,以下两种情况可能不适用于 Electron:

  1. 在 web 场景下,UI 元素操作比较卡顿,达到瓶颈,必须采用性能更高的原生 UI。不过我建议不要轻易下这个结论,一般情况下这种性能问题都是写的代码不够极致,建议先从 web 的角度进行性能优化(比如,长列表场景我们可以通过压缩合成层优化性能来数十倍地提高性能)。

  2. 对某一项技术有深度依赖,而这项技术在 web 方面存在性能上的天花板。事实上这种情况也并不多见,其中一个合理的场景是视频相关的应用,比如视频会议,或者视频播放器,这类由于 Chrome 本身的渲染流水线的限制,使用 video 标签或者使用 WebGL 都会存在一些性能问题,这个时候我们需要更深入的去进行相关能力的定制,就需要从 Electron 的框架中跳脱出来,或者针对 Electron 进行二次开发。

关于 WebGL: 实际上很多 web 开发者会把 WebGL 当作部分场景下性能优化的银弹,但实际上 WebGL 目前存在诸多困境:WebGL 1.0 虽然已经普及,但是其作为 OpenGL ES 2.0 的子集,性能上已经并不特别适用现代硬件架构;而 WebGL 2.0 目前仍然在普及中并且各家厂商意见无法一致;Web GPU 可能是一个更好的解决方案,底层直接对接 D3D12、metal、vulkan 等更底层更先进的图形框架,但目前成熟度不高。

如果你的应用在经过以上分析之后,认为仍然可以使用 Electron 进行开发,那么恭喜你拥有了一个如此高效率的开发方案(如果不行,建议你可以选择其他的解决方案,比如 QT)。

当然在此基础上,我们仍然需要进行充足的性能优化和稳健的架构设计,来让我们应用的可靠性变得更高。

性能优化

和 web 不同的是,我们的 native 应用需要更加关注如下三个指标:

1. cpu

cpu 占用相关的问题,我们在 web 技术栈中一般也会关注,不过更多的是关注函数的调用耗时,是否存在同步调用的耗时过长导致卡顿等问题。

而在桌面应用程序的场景中,我们需要从整个应用的维度关注 cpu 消耗,并且需要更加重视。

另外一个原因是,在网页场景中,页面的 cpu 占用通常不会特别直观地被用户发现(因为系统层面通常只会体现在浏览器占用 cpu 较多),而在现在的原生场景,用户可以直接在任务管理器中看到我们的应用,如果我们的应用持续有一个较高的 cpu 占用,就会比较容易被用户发现,甚至触发系统告警提示强杀应用,这对我们应用的口碑也是一个比较负面的影响。

2. 内存

在桌面应用程序中,内存的使用方式有了一个明显的变化:

原有的 web 页面,通常是用完即走,而对于 native 应用用户一般会打开很久,这也就意味着我们如果一旦产生内存泄漏或者内存占用比较高的情况,对用户的影响是持续并且被不断放大的。

对于 cpu 和内存的分析,我们可以通过以下方式:

  1. 开发阶段通过 visual studio 或 instruments 来详细分析我们开发的功能的 cpu 和内存分配情况,发现问题。
  2. 测试发布阶段通过第三方内存分析工具,流程化的分析 cpu 和内存占用并产出报告。
  3. 线上阶段持续监控 cpu 和内存消耗情况,并且上报数据进行统计和监控告警。

3. crash 率

实际上在前端领域基本上没有 “crash” 这个说法,不过对于 native 应用来说,即使我们的应用是完全采用前端技术栈,也可能存在 crash (crash 在 Electron 的代码),一般这个时候用户的体验是闪退,相对来说算是严重影响用户体验的问题,因此值得我们足够的重视。

对于 crash 问题我们应该做好以下三点:

  1. 运行时 crash 监听机制,一般是 sentry 或者直接使用其依赖的 crash_pad。
  2. 符号管理机制,管理我们原生模块,和我们用到的 Electron 对应版本的符号。
  3. 运行时 crash 上报告警机制。

架构优化

除了上述性能指标和监控手段,我们可以通过一定的架构优化,来增强系统的可靠性。

通过 Node.js addon 或者独立进程的方式原生实现非 UI 内容

这里的作用主要是希望能够借助原生模块的高性能优化 cpu 的占用。

Electron 让我们开发 ui 相关的页面变得非常高效,但是一些逻辑部分,或者和操作系统进行交互的部分,我们还是需要原生开发的手段,毕竟即使使用了 Node.js,也无法直接进行系统调用。

这里我们可以采用 Node.js addon 的方式或者独立进程+进程间通信的方式,两者的好处分别是:

addon:

  1. 方便进行内存共享。

独立进程:

  1. 通常会增加可靠性,独立进程挂掉后可以单独重启,不影响用户界面。
  2. 需要防止大块的内存重复占用,可以通过共享内存等方式来进行优化。

减少或者禁止在渲染进程使用 remote

有的时候,即使 electron 的技术选型适合你的项目,但如果滥用 remote 也会造成整个应用的大量不稳定与卡顿。

实际上,我们可以通过阅读 electron 的源代码发现,remote 模块只是对 IPC 消息的同步封装,方便渲染进程调用主进程的对象和方法,而不必显式发送消息进行进程间通信。所以,由于其屏蔽了内部的进程间通信,在调用的时候基本无感主进程的存在和 IPC 的风险,但事实上这却有卡顿甚至卡死渲染进程的风险。

另外,去掉 remote 还有另外一个好处,就是方便我们项目的 PC 版本和 web 版本进行同构,具有更高的可维护性。

所以针对一般的项目,笔者建议能禁用就禁用 remote,规避此隐患。

其他优化

我们可以在代码编写和打包的过程中,做一些其他的优化,在这里,大部分前端的优化比如动态加载、代码分割、图片缓存等大多也都适用 electron 的情况,除此之外,还有一些优化则是:

  1. 避免重复打包:

    • 避免 node_modules 和 webpack 重复的打包和引入,对于 webpack 我们可以使用 webpack-bundle-analyzer 来分析打包体积进行优化
    • 减少无关文件的打包,可以通过配置针对 electron-builder 的 config 去除无关内容打包,同时可以使用 node-prune 来去除无用的 node_modules 小文件。
  2. v8-code-cache:

  3. 更多可以参考 VSCode 的相关分享:https://www.youtube.com/watch?v=r0OeHRUCCb4