Skip to content

Latest commit

 

History

History
240 lines (139 loc) · 17.1 KB

File metadata and controls

240 lines (139 loc) · 17.1 KB

开发人员经常询问优化 Electron 应用性能的策略。软件工程师、用户和框架开发者对 "性能 "的定义并不总是一致的。本文档概述了一些 Electron 维护者最喜欢的方法,以减少内存、CPU 和磁盘资源的使用量,同时确保应用程序对用户输入做出响应,并尽快完成操作。此外,我们希望所有的性能策略都能为应用程序的安全性保持高标准。

有关如何使用 JavaScript 构建高性能网站的智慧和信息通常也适用于 Electron 应用程序。在某种程度上,讨论如何构建高性能 Node.js 应用程序的资源也同样适用,但要注意理解,"性能 "一词对于 Node.js 后端和客户端上运行的应用程序的意义是不同的。

本列表仅为方便您使用而提供,与我们的[安全清单](https://www.electronjs.org/docs/latest/tutorial/security)一样,并非详尽无遗。也许你可以按照下面列出的所有步骤来构建一个缓慢的 Electron 应用程序。Electron 是一个强大的开发平台,开发者可以随心所欲。所有这些自由意味着性能在很大程度上是你的责任。

测量,测量,测量 Measure, Measure, Measure

下面列出了一些相当直接且易于实施的步骤。但是,要构建性能最佳的应用程序版本,您需要采取更多步骤。相反,您必须通过仔细分析和测量来仔细检查应用程序中运行的所有代码。瓶颈在哪里?当用户点击按钮时,哪些操作占用了大量时间?当应用程序处于闲置状态时,哪些对象占用的内存最多?

我们一次又一次地发现,构建高性能 Electron 应用程序的最成功策略是剖析运行中的代码,找出其中最耗费资源的部分,并对其进行优化。反复重复这一看似费力的过程,将极大地提高应用程序的性能。使用 Visual Studio Code 或 Slack 等大型应用程序的经验表明,这种做法是迄今为止提高性能最可靠的策略。

要进一步了解如何剖析应用程序的代码,请熟悉 Chrome 浏览器开发工具。如需同时对多个进程进行高级分析,请考虑使用 Chrome 浏览器跟踪工具。

推荐阅读

阻塞性能清单

如果尝试这些步骤,您的应用程序很可能会更精简、更快速,而且对资源的消耗也会更少。

1、不小心引入的模块

在将 Node.js 模块添加到您的应用程序之前,请检查该模块。该模块包含多少依赖项?它需要什么样的资源才能在 require() 语句中简单调用?你可能会发现,在 NPM 软件包注册表上下载次数最多或在 GitHub 上获得最多星星的模块,实际上并不是最精简或最小的模块。

为什么呢?

我们可以通过一个真实的例子来说明这一建议背后的原因。在 Electron 的早期,网络连接的可靠检测是一个问题,导致许多应用程序使用一个暴露了简单 isOnline() 方法的模块。

该模块通过尝试连接一些知名端点来检测您的网络连接。至于这些端点的列表,它依赖于一个不同的模块,该模块也包含一个众所周知的端口列表。这个依赖关系本身依赖于一个包含端口信息的模块,该模块以 JSON 文件的形式存在,内容超过 10 万行。每当加载模块时(通常是在 require('module') 语句中),模块就会加载其所有依赖项,并最终读取和解析该 JSON 文件。解析数千行 JSON 是一项非常昂贵的操作。在速度较慢的机器上,可能需要花费整整几秒钟的时间。

在许多服务器环境中,启动时间几乎无关紧要。一个需要所有端口信息的 Node.js 服务器,如果能在服务器启动时将所有需要的信息加载到内存中,那么它的性能实际上可能会 "更强",这样做的好处是能更快地处理请求。本例中讨论的模块并非 "坏 "模块。但是,Electron 应用程序不应加载、解析并在内存中存储实际上并不需要的信息。

简而言之,一个主要为运行 Linux 的 Node.js 服务器编写的看似优秀的模块,可能会对应用程序的性能造成影响。在这个特殊的例子中,正确的解决方案是不使用任何模块,而是使用 Chromium 后续版本中包含的连接性检查。

如何做到?

在考虑模块时,我们建议您检查: 1、所引入的依赖的大小 2、加载 (require()) 所需的资源 3、执行您感兴趣的操作所需的资源

只需在命令行下达一条命令,就能为加载模块生成 CPU 配置文件和堆内存配置文件。在下面的示例中,我们将查看流行的模块请求

node --cpu-prof --heap-prof -e "require('request')"

执行该命令后,会在执行该命令的目录下生成一个 .cpuprofile 文件和一个 .heapprofile 文件。这两个文件都可以使用 Chrome 浏览器开发工具的 "性能 "和 "内存 "选项卡进行分析。 alt text

alt text

在这个例子中,在作者的机器上,我们看到加载请求耗时将近半秒,而节点撷取占用的内存大幅减少,不到 50 毫秒。

2、过早加载和运行代码

如果有昂贵的设置操作,请考虑推迟这些操作。检查应用程序启动后立即执行的所有工作。与其立即执行所有操作,不如考虑按照更贴近用户体验的顺序交错执行。

在传统的 Node.js 开发中,我们习惯于将所有 require() 语句放在顶部。如果你正在使用同样的策略编写 Electron 应用程序,并且正在使用不急需的大型模块,那么请使用同样的策略,将加载推迟到更合适的时间。

为什么?

加载模块是一项非常昂贵的操作,尤其是在 Windows 系统上。应用程序启动时,不应让用户等待目前不需要的操作。

这看似显而易见,但许多应用程序往往会在启动后立即执行大量工作,如检查更新、下载稍后流程中使用的内容或执行繁重的磁盘 I/O 操作。

让我们以 Visual Studio Code 为例。当你打开一个文件时,它会立即将文件显示给你,而不显示任何代码高亮,优先考虑你与文本交互的能力。一旦完成这项工作,它就会开始高亮显示代码。

怎么做?

让我们举个例子,假设你的应用程序正在解析虚构的 .foo 格式文件。为此,它依赖于同样虚构的 foo-parser 模块。在传统的 Node.js 开发中,您可能会编写急于加载依赖项的代码:

const fs = require("node:fs");
const fooParser = require("foo-parser");

class Parser {
    constructor() {
        this.files = fs.readdirSync(".");
    }

    getParsedFiles() {
        return fooParser.parse(this.files);
    }
}

const parser = new Parser();

module.exports = { parser };

在上述示例中,我们正在进行大量工作,这些工作在文件加载后立即执行。我们需要立即获取解析后的文件吗?我们能不能稍晚一些,在实际调用 getParsedFiles() 时再做这项工作?

// "fs" is likely already being loaded, so the `require()` call is cheap
const fs = require("node:fs");

class Parser {
    async getFiles() {
        // Touch the disk as soon as `getFiles` is called, not sooner.
        // Also, ensure that we're not blocking other operations by using
        // the asynchronous version.
        this.files = this.files || (await fs.promises.readdir("."));

        return this.files;
    }

    async getParsedFiles() {
        // Our fictitious foo-parser is a big and expensive module to load, so
        // defer that work until we actually need to parse files.
        // Since `require()` comes with a module cache, the `require()` call
        // will only be expensive once - subsequent calls of `getParsedFiles()`
        // will be faster.
        const fooParser = require("foo-parser");
        const files = await this.getFiles();

        return fooParser.parse(files);
    }
}

// This operation is now a lot cheaper than in our previous example
const parser = new Parser();

module.exports = { parser };

简而言之,"及时 "分配资源,而不是在应用程序启动时分配所有资源。

3、阻塞主进程

Electron 的主进程(有时称为 "浏览器进程")很特别:它是应用程序所有其他进程的父进程,也是操作系统与之交互的主要进程。它负责处理窗口、交互以及应用程序内各组件之间的通信。它也是用户界面线程的所在。

在任何情况下,都不应阻塞该进程和 UI 线程的长期运行操作。阻塞用户界面线程意味着整个应用程序将冻结,直到主进程准备好继续处理。

为什么?

主进程及其 UI 线程基本上是应用程序内部主要操作的控制塔。当操作系统告诉您的应用程序有鼠标点击时,它会先经过主进程,然后再到达您的窗口。如果您的窗口正在渲染一个流畅的动画,那么它需要与 GPU 进程进行对话--再次通过主进程。

Electron 和 Chromium 会小心地将繁重的磁盘 I/O 和 CPU 绑定操作放到新线程上,以避免阻塞用户界面线程。你也应该这样做。

如何做?

Electron 强大的多进程架构可随时协助你完成长期任务,但也有少量性能陷阱。

1、对于长期运行的 CPU 负荷较大的任务,可使用工作线程,考虑将其移至 BrowserWindow,或(作为最后手段)生成一个专用进程。 2、尽可能避免使用同步 IPC 和 @electron/remote 模块。虽然有合理的使用情况,但在不知情的情况下阻塞用户界面线程实在太容易了。 3、避免在主进程中使用阻塞 I/O 操作。简而言之,只要 Node.js 核心模块(如 fs 或 child_process)提供同步或异步版本,您就应该优先选择异步和非阻塞变体。

4、阻塞渲染进程

由于 Electron 搭载的是当前版本的 Chrome 浏览器,因此你可以利用网络平台提供的最新、最强大的功能来推迟或卸载繁重的操作,从而保持应用程序的流畅性和响应速度。

为什么?

您的应用程序可能需要在渲染器进程中运行大量 JavaScript。诀窍在于尽可能快速地执行操作,同时不占用保持滚动流畅、响应用户输入或 60fps 动画所需的资源。

如果用户抱怨您的应用程序有时会 "卡顿",那么在渲染器代码中协调操作流就特别有用。

怎么做?

一般来说,为现代浏览器构建高性能网络应用程序的所有建议也适用于 Electron 的渲染器。目前,您可以使用的两个主要工具是用于小型操作的 requestIdleCallback()和用于长期运行操作的 Web Workers。

requestIdleCallback() 允许开发人员在进程进入空闲期时排队执行一个函数。通过它,您可以在不影响用户体验的情况下执行低优先级或后台工作。有关如何使用的更多信息,请查看 MDN 上的文档

Web Workers 是在独立线程上运行代码的强大工具。有一些注意事项需要考虑--请查阅 Electron 的多线程文档Web Worker 的 MDN 文档。对于任何需要长时间使用大量 CPU 的操作,Web Worker 都是理想的解决方案。

5、不必要的多填充 Unnecessary polyfills

Electron 的一大优势是,你可以清楚地知道哪个引擎会解析你的 JavaScript、HTML 和 CSS。如果您要重新利用为 web 编写的代码,请确保不要使用 Electron 中的 polyfill 功能。

为什么?

在为当今互联网构建 Web 应用程序时,最古老的环境决定了你能使用和不能使用哪些功能。即使 Electron 支持性能良好的 CSS 过滤器和动画,但旧版浏览器可能不支持。在可以使用 WebGL 的情况下,开发人员可能会选择更耗费资源的解决方案来支持旧版手机。

说到 JavaScript,您可能已经包含了用于 DOM 选择器的 jQuery 等工具包库,或用于支持 async/await 的 regenerator-runtime 等 polyfills。

在 Electron 中,基于 JavaScript 的 polyfill 很少会比同等的本地功能更快。不要通过提供自己版本的标准 Web 平台功能来降低 Electron 应用程序的速度。

怎么做?

假定当前版本的 Electron 不需要多填充功能。如果有疑问,请查看 caniuse.com,检查你的 Electron 版本中使用的 Chromium 是否支持你想要的功能。

此外,请仔细检查您使用的库,它们是否真的有必要。例如,jQuery 是如此成功,以至于它的许多功能现在已经成为可用的标准 JavaScript 功能集的一部分。

如果你使用的是 TypeScript 之类的转译器/编译器,请检查其配置,确保你使用的是 Electron 支持的最新 ECMAScript 版本。

6、不必要或阻塞的网络请求

如果可以轻松地将很少变化的资源与应用程序捆绑在一起,则应避免从互联网上获取这些资源。

为什么?

许多使用 Electron 的用户一开始都是将一个完全基于 Web 的应用程序转化为桌面应用程序。作为 Web 开发人员,我们习惯于从各种内容交付网络(CDN)加载资源。现在,你正在发布一个合适的桌面应用程序,请尽可能地 "剪断 "线缆(cut the cord),避免让你的用户等待那些永远不会改变的资源,这些资源可以很容易地包含在你的应用程序中。

谷歌字体就是一个典型的例子。许多开发者都在使用谷歌的免费字体库,该库还附带了一个内容交付网络。它的优势很直接: 只需包含几行 CSS,剩下的就交给 Google 吧。

在制作 Electron 应用程序时,如果能下载字体并将其包含在应用程序的捆绑包中,就能更好地为用户服务。

怎么做?

在理想状态下,您的应用程序根本不需要网络就能运行。要达到这一目标,您必须了解您的应用程序正在下载哪些资源,以及这些资源有多大。

为此,请打开开发者工具。导航至 "网络 "选项卡,选中 "禁用缓存 "选项。然后,重新加载你的渲染现器。除非你的应用程序禁止这种重载,否则你通常可以在开发工具处于焦点状态时点击 Cmd + R 或 Ctrl + R 来触发重载。

现在,这些工具将仔细记录所有网络请求。第一遍,清点所有下载的资源,首先关注较大的文件。其中是否有不会更改的图片、字体或媒体文件,可以包含在捆绑包中(bundle)?如果有,请将它们包括在内。

下一步,启用网络节流。找到当前为 "在线 "的下拉菜单,选择较慢的速度,如快速 3G。重新加载你的渲染器,看看你的应用程序是否有不必要的资源等待。在许多情况下,应用程序会等待网络请求完成,尽管实际上并不需要相关资源。

作为提示,从互联网加载您可能想要更改的资源,而无需发送应用程序更新,是一种强大的策略。如果想对资源加载方式进行高级控制,可以考虑使用 Service Worker

7、打包您的代码

正如在 "过早加载和运行代码 "一文中指出的,调用 require() 是一项昂贵的操作。如果可以,请将应用程序的代码捆绑到一个文件中。

为什么?

现代 JavaScript 开发通常涉及许多文件和模块。虽然这对使用 Electron 进行开发完全没有问题,但我们强烈建议你将所有代码捆绑到一个文件中,以确保调用 require() 时的开销只在应用程序加载时支付一次。

怎么做?

目前有许多 JavaScript 捆绑程序,我们知道最好不要推荐一种工具而不是另一种,以免激怒社区。不过,我们建议您使用能够处理 Electron 独特环境的捆绑工具,因为它需要同时处理 Node.js 和浏览器环境。

截至撰写本文时,流行的选择包括 Webpack、Parcel 和 rollup.js。

8、不需要默认菜单时,调用 Menu.setApplicationMenu(null)

Electron 会在启动时设置一个默认菜单,其中包含一些标准条目。但是,你的应用程序可能会出于某些原因需要更改菜单,这将有利于提高启动性能。

为什么?

如果你创建了自己的菜单或使用了无框架窗口而没有原生菜单,你应该尽早告诉 Electron 不要设置默认菜单。

如何做?

Call Menu.setApplicationMenu(null) before app.on("ready"). This will prevent Electron from setting a default menu. See also electron/electron#35512 for a related discussion.