电子是使用同一代码库为不同系统构建桌面应用程序的流行框架。
但是,我们经常听到它很慢,消耗了很多内存,并产生了多个过程,从而减慢整个系统。一些非常受欢迎的应用程序是使用电子构建的,包括:
- Microsoft团队(但他们正在迁移到Edge WebView2),
- 信号,
- WhatsApp。
并非所有这些都是完美的,但是有一些非常好的示例,例如Visual Studio代码。我们可以说这很慢吗?根据我们的经验,相反的表现和反应迅速。
在本文中,我们将向您展示如何减少电子应用中的瓶颈并使其快速!提出的方法可以应用于基于node.js的应用程序,例如API服务器或其他需要高性能的工具。
基于电子的游戏发射器是我们的测试主题
我们的项目是基于电子的游戏启动器。如果您玩游戏,则可能在计算机上安装了其中一些。大多数启动器下载游戏文件,安装更新和验证文件,因此游戏可以启动而不会出现任何问题。
我们可以加快速度的部分取决于连接速度,但是在验证下载或修补的文件时,这是一个不同的故事,如果游戏很大,那么整个过程可能会花费大量时间。这是我们的情况。
我们的应用程序负责下载文件,如果有资格,则应用二进制补丁。完成此操作后,我们必须确保没有任何损坏。造成腐败的原因无关紧要,我们的用户想玩游戏,我们必须使其成为可能。
现在,让我给你一些数字。我们的游戏由44个总尺寸约为4.7GB的文件组成。
下载游戏或更新后,我们必须验证所有内容。我们使用https://www.npmjs.com/package/crc计算每个文件的CRC并根据清单文件进行验证,让我们看看这种方法的性能,某些基准测试的时间。
。运行Electron App绩效优化基准测试
所有基准测试均在2021 MacBook Pro14âm1Pro。
上运行首先,我们需要一些文件来验证。我们可以使用命令
创建一些mkfile -n 200m test_200m_1
但是,如果我们看内容,我们将看到所有零!
➜ /tmp cat test_200m_1 | xxd | tail -n 10
0c7fff60: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fff70: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fff80: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fff90: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fffa0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fffb0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fffc0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fffd0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7fffe0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0c7ffff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
这可能会给我们带来偏斜的结果。相反,我们将使用此命令
dd if=/dev/urandom of=test_200m_1 bs=1M count=200
我将创建10个文件,每个文件200MB,并且由于它们中的数据是随机的,所以它们应该具有不同的校验和。
基准代码:
import crc32 from "crc/crc32";
import { createReadStream } from "fs";
const calculate = async (path) => {
return new Promise((resolve, reject) => {
let checksum = null;
const readStream = createReadStream(path);
readStream.on("data", (chunk) => {
checksum = crc32(chunk, checksum);
});
readStream.on("end", () => {
resolve(checksum);
});
readStream.on("error", () => {
resolve(false);
});
});
};
const then = new Date().getTime();
await calculate("test_200m_1");
const now = new Date().getTime();
const elapsedTimeInMs = now - then;
console.log(elapsedTimeInMs);
创建读取流并逐步计算校验和计算大约需要800毫秒。我们更喜欢流,因为我们可以负担得起将大文件加载到系统内存中。如果我们一一计算所有文件的CRC32,则结果是〜16700ms。它在第三文件后放慢脚步。
如果我们使用 Promise.All 同时运行它们,这会更好吗?好吧…这是测量误差的极限。它在〜16100ms左右变化。
所以,这是我们到目前为止的结果:
单文件 | 10个文件一个 | Promise.All | 中的10个文件
---|---|---|
〜800ms | 〜16700ms | 〜16100ms |
优化电子应用程序性能的可能方法
在优化电子应用时,您可以采取许多路径,但我们对:
- NodeJS Worker Threads
- Node-API
- Neon
- Napi-rs
- 其他可在本地工作的JS库
nodejs工作线程
Worker线程需要周围的一些样板代码。另外,如果您的代码基库在打字稿中,则可能是有问题的,它是可行的,但需要其他工具,例如TS节点或配置。我们不想产生谁知道有多少工人线程也会效率低下。性能问题在其他地方。无论我们在何处,它都会很慢。
结论:产卵工具线程将使我们的应用程序更慢,因此Nodejs Worker线程不适合我们。
节点-API
如果我们想要快速,Node-API看起来像是一个完美的解决方案。用C/C ++编写的库必须很快。如果您希望在C上使用C ++,则node-addon-api可以提供帮助。这可能是可用的最佳解决方案之一,尤其是因为它得到了Node.js团队的正式支持。它是一旦建造的超级稳定,但是在开发过程中可能会很痛苦。错误通常远非易于理解,因此,如果您不是C的专家,它可能很容易踢屁股。
结论:我们没有C技能来解决错误,因此Node-api不适合我们。
霓虹灯结合
现在它变得有趣,霓虹灯绑定。 Node.js中的Rust听起来很棒,这是另一个流行语,但这只是一个流行语吗?霓虹灯说,它是由1Password和Signal https://neon-bindings.com/docs/example-projects之类的流行应用程序使用的,但让我们看一下基于Rust的其他选项,即NAPI-RS。
结论:霓虹灯绑定看起来很有希望,但让我们看看它与我们的最后一个选择的比较。
每日r
如果我们查看文档,NAPI-RS的文档看起来比Neon的文档好得多。该框架是由行业中一些知名人士赞助的。大品牌的广泛文档和支持是我们使用NAPI-R而不是霓虹灯绑定的充分理由。
结论:NAPI-RS比可比的霓虹灯提供了更好的文档,因此做出更安全的选择。
使用NAPI-RS优化电子应用程序性能
为了优化我们的电子应用,我们将使用NAPI-RS,将Rust与Node.js混合在一起。由于其性能,记忆安全,社区和工具(货物,生锈的安分析仪),Rust是Node.js的有吸引力的补充。难怪它是最喜欢的语言之一,为什么越来越多的公司将其模块重写为生锈。
使用NAPI-RS,我们需要构建一个包含https://crates.io/crates/crc32fast的库,以非常快地计算CRC32。 NAPI-RS为我们提供了构建NPM软件包的出色现成工作流程,因此将其构建并与该项目集成在一起很容易。也支持预先构建,因此您根本不需要生锈环境来使用它,将下载和使用正确的构建。无论您使用Windows,Linux还是MacOS(Apple M1机器也在列表中。)
使用CRC32Fast库,我们将使用Hasher实例从读取流中更新校验和,如JS实现:
// Spawn and run the thread, it starts immediately
let handle = thread::spawn(move || {
// Has to be equal to JS implementation, it changes the checksum if different
const BUFFER_LEN: usize = 64 * 1024;
let mut buffer = [0u8; BUFFER_LEN];
// Open the file, if it fails it will return -1 checksum.
let mut f = match File::open(path) {
Ok(f) => f,
Err(_) => {
return -1;
}
};
// Hasher instance, allows us to calculate checksum for chunks
let mut hasher = Hasher::new();
loop {
// Read bytes and put them in the buffer, again, return -1 if fails
let read_count = match f.read(&mut buffer[..]) {
Ok(count) => count,
Err(_) => {
return -1;
}
};
// If this is the last chunk, read_count will be smaller than BUFFER_LEN.
// In this case we need to shrink the buffer, we don't want to calculate the checksum for a half-filled buffer.
if read_count != BUFFER_LEN {
let last_buffer = &buffer[0..read_count];
hasher.update(&last_buffer);
} else {
hasher.update(&buffer);
}
// Stop processing if this is the last chunk
if read_count != BUFFER_LEN {
break;
}
}
// Calculate the "final" checksum and return it from thread
let checksum = i64::from(hasher.finalize());
checksum
运行Electron App绩效优化基准测试
听起来像是假或无效的结果,但单个文件仅75ms!它比JS实施快十倍。当我们一一处理所有文件时,它大约是730ms,因此它的扩展也更好。
但这不是全部。我们可以进行另外一个非常简单的优化。我们可以让它接受路径数组并为每个文件衍生一个线程。
记住:RUST对线程的数量没有限制,因为这些是由系统管理的OS线程。这取决于系统,因此,如果您知道要产生多少个线程并且不是很高,那么您应该安全。否则,我们建议将限制和处理文件或在块中进行计算。
让我们的计算将每个文件的线程放在线程中,然后一次返回所有校验和
// Vector of threads, to be "awaited" later
let mut threads = Vec::<std::thread::JoinHandle<i64>>::new();
for path in paths.into_iter() {
// Spawn and run the thread, it starts immediately
let handle = thread::spawn(move || {
// ... code removed for brevity
});
// Push handle to the vector
threads.push(handle);
}
// Prepare an empty vector for checksums
let mut results = Vec::<i64>::new();
// Go through every thread and wait for it to finish
for task in threads.into_iter() {
// Get the checksum and push it to the vector
let result = task.join().unwrap();
results.push(result);
}
// Return vector(array) of checksums to JS
Ok(results)
用一系列路径调用本机函数需要多长时间?
进行所有计算?只有150ms,是的,这很快。可以肯定的是,我们重新启动了MacBook并进行了另外两项测试。
第一次运行:
Rust took 463ms Checksums [
2918571326, 644605025,
887396193, 1902706446,
2840008691, 3721571342,
2187137076, 2024701528,
3895033490, 2349731754
]
JS promise.all took 16190ms Checksum [
2918571326, 644605025,
887396193, 1902706446,
2840008691, 3721571342,
2187137076, 2024701528,
3895033490, 2349731754
]
第二次运行:
Rust took 197ms Checksums [
2918571326, 644605025,
887396193, 1902706446,
2840008691, 3721571342,
2187137076, 2024701528,
3895033490, 2349731754
]
JS promise.all took 16189ms Checksum [
2918571326, 644605025,
887396193, 1902706446,
2840008691, 3721571342,
2187137076, 2024701528,
3895033490, 2349731754
]
让我们将所有结果汇总在一起,看看它们如何比较。
js | Rust | |
---|---|---|
单个文件 | 〜800ms | 〜75ms |
10个文件一个 | 〜16700ms | 〜730ms |
10个文件Promise.All | 〜16100ms | - |
线程中的10个文件 | - | 〜200ms |
值得注意的是,用空阵列调用本机功能为124584纳米秒为0.12ms,因此开销很小。
记住要使电子应用打开包装
一开始,所有这些都适用于Web API,CLI工具和电子。基本上,使用node.js的所有内容。但是有了电子,还有一件事要记住。电子将应用程序捆绑到一个名为app.asar的档案中。必须解开某些节点模块才能由运行时加载。大多数像电子构建器或Forge这样的捆绑器会自动将这些模块保存在存档文件之外,但是可能会发生我们的库将留在ASAR文件中。如果是这样,则应指定应保留哪些库。它不是强制性的,但会减少解开和加载这些。节点文件的开销。
我们的建议:尝试使用Rust和C进行尝试以提高您的电子应用程序性能
您可以看到,有多种方法可以加快电子应用程序的一部分,尤其是在进行大量计算方面。幸运的是,开发人员可以从不同的语言和策略中进行选择以涵盖各种用例。
在我们的应用程序中,验证文件只是整个启动器过程的一部分。对于大多数玩家来说,最慢的部分是下载文件,但不能超越您的互联网服务提供商提供的内容。另外,有些玩家的机器上有HDD磁盘,其中IO可能是瓶颈,而不是CPU。
但是,如果我们可以以合理的成本来改进并提高性能,那么我们应该为此而努力。如果您的应用程序中有任何功能或模块可以在Rust或C中重写,为什么不尝试实验呢?这种优化可以大大改善您的应用程序的整体性能。