Electron 性能

2023-02-16 17:15 更新

開發(fā)者經(jīng)常詢問優(yōu)化 Electron 應(yīng)用程序性能的策略。 軟件工程師、用戶和框架開發(fā)者并不總是就“性能”的含義達成單一定義。 此文檔概述了Electron 維護者最喜歡的減少內(nèi)存使用、 CPU 負載以及磁盤資源使用的方式。以確保您的應(yīng)用程序能夠響應(yīng)用戶輸入并盡快完成操作。 此外,我們希望所有的性能策略都能保持您應(yīng)用的高標(biāo)準(zhǔn)安全。

關(guān)于如何使用 JavaScript構(gòu)建高性能網(wǎng)站的技巧和方法通常也適用于Electron 應(yīng)用程序。 在某種程度上,討論如何構(gòu)建高性能 Node.js 應(yīng)用的方法同樣也適用。但是小心理解“性能”一詞的含義對于 Node.js 后端和客戶端程序并不相同。

文中的列表提供了一些方便,同時也需要注意,它和我們的 安全性檢查列表 類似,并不詳盡。 即使你參照了下面提到的所有步驟,依然有可能構(gòu)建出來一個性能低的Electron應(yīng)用。 Electron是一個強大的開發(fā)平臺,可以讓開發(fā)人員按照自己所想,做更多的或更少的事情。 而這種自由的代價就是開發(fā)者需要承擔(dān)大部分性能上的責(zé)任。

再三權(quán)衡?

以下列舉了一些直截了當(dāng)、易于實現(xiàn)的方式。 但是,如果你想構(gòu)建性能最優(yōu)秀的應(yīng)用,僅僅這些是不夠的。 你需要仔細檢查應(yīng)用中運行的所有代碼,認真地進行分析和衡量。 瓶頸在哪里? 當(dāng)用戶點擊按鈕時,哪些操作的執(zhí)行占用了最多的時間? 當(dāng)應(yīng)用程序被掛起時,哪些對象占用了最多的內(nèi)存?

通過多次的嘗試,我們發(fā)現(xiàn),構(gòu)建高性能的Electron應(yīng)用程序,最成功的策略是分析正在運行的代碼,查找其中最耗資源的部分,然后對其進行優(yōu)化。 一遍又一遍地重復(fù)這個“搬磚”的過程,將極大地提高應(yīng)用程序的性能。 在大型應(yīng)用程序(例如Visual Studio Code、Slack)中的實踐經(jīng)驗證明了這是目前最可靠的性能提升策略。

要了解更多關(guān)于如何分析應(yīng)用程序代碼的信息,請熟悉Chrome開發(fā)者工具。 若要高級分析查看多個進程,請使用 Chrome Tracing 工具。

推薦閱讀?

清單:性能建議?

如果你嘗試這些步驟,你的應(yīng)用可能會略微簡潔、快速,而且一般來說會更少出現(xiàn)資源不足的情況。

  1. 謹慎地加載模塊
  2. 過早的加載和執(zhí)行代碼
  3. 阻塞主進程
  4. 阻塞渲染進程
  5. 不必要的polyfills
  6. 不必要的或者阻塞的網(wǎng)絡(luò)請求
  7. 打包你的代碼

1. 謹慎地加載模塊?

在向你的應(yīng)用程序添加一個 Node.js 模塊之前,請檢查這個模塊。 這個模塊包含了多少依賴? 簡單的一個?require()?聲明中包含了什么種類的資源? 你可能發(fā)現(xiàn)NPM包注冊的最多的或者Github上Star最多的模塊實際上并不是最簡單或者最小可用的模塊。

為什么?

這一建議背后的理由最好用一個真實的例子來說明。 在Electron最開始的那些日子,可靠的檢查網(wǎng)絡(luò)連接是一個問題,導(dǎo)致很多應(yīng)用公開的使用了一個簡單的isOnline()方法。

該模塊通過嘗試訪問多個眾所周知的端點,檢測到您的網(wǎng)絡(luò)連接。 至于這些資源的列表,它取決于一個完全不同的模塊,這個模塊也包含一個眾所周知的端口。 這個依賴本身又依賴一個包含超過 100 000行端口信息的JSON文件的模塊。 每當(dāng)模塊加載時(通常用 require('module')),它會加載所有依賴關(guān)系并最終讀取并解析此 JSON 文件。 解析幾千行的JSON是一個非常繁重的操作。 在性能差的機器上,它會占用整整幾秒的時間。

在許多服務(wù)器環(huán)境中,啟動時間幾乎無關(guān)緊要。 一個Node.js 服務(wù)器要求所有端口的信息可能實際上是“性能更好” 如果服務(wù)器在啟動時將所有需要的信息加載到內(nèi)存,這樣就能更快地為響應(yīng)請求。 此示例中討論的模塊不是一個“壞”模塊。 然而,Electron 應(yīng)用不應(yīng)該將實際上不需要的信息加載、解析和存儲在內(nèi)存中。

簡而言之,一個主要為運行在Linux系統(tǒng)上的Node.js 服務(wù)器編寫的模塊,雖然看起來很好,但是對你的應(yīng)用性能來說可能是個壞消息。 在這個特殊的示例中,正確的解決方案是根本不需要加載模塊, 而是使用了一 個包含在以后版本的 Chromium 中的連接性檢查。

怎么做?

當(dāng)考慮一個模塊時,我們建議你做以下檢查:

  1. 包含的依賴項的大小
  2. 需要加載的(?require()?) 資源
  3. 你所加載的資源能夠執(zhí)行你關(guān)心的操作

可以使用命令行上的單個命令生成用于加載模塊的 CPU 配置文件和堆內(nèi)存配置文件 在下面的示例中,我們看一下受歡迎的模塊 request。

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

執(zhí)行此命令將在您執(zhí)行的目錄下生成一個.cpuprofile和一個.heapprofile 文件。 這兩個文件都可以使用 Chrome 開發(fā)者工具進行分析,分別使用 Performance 和 Memory 標(biāo)簽 進行分析。



在這個例子里,我們看到在作者的機器上加載request 大概用了半秒鐘,其中 node-fetch明顯占用了極少的內(nèi)存并且加載用時少于 50ms。

2. 過早的加載和執(zhí)行代碼

如果你有非常繁重的初始化操作,請考慮推遲進行。 程序啟動立刻查看應(yīng)用執(zhí)行的全部工作。 考慮按照用戶操作的順序?qū)⑺鼈冨e開執(zhí)行,而不是立刻執(zhí)行所有的操作。

在傳統(tǒng)的Node.js開發(fā)中,我們習(xí)慣將所有的require()語句放在代碼頂部。 如果你目前正在使用相同的策略and并且使用你不需要立即加載的大型模塊編寫你的 Electron 應(yīng)用程序,使用相同的策略并推遲到更適當(dāng)?shù)臅r機加載。

為什么?

加載模塊是令人吃驚的繁重的操作,尤其是在Windows上。 當(dāng)你的應(yīng)用開始,不應(yīng)該讓用戶等待當(dāng)時不需要的操作。

這似乎是顯而易見的, 但許多應(yīng)用程序在程序啟動后可能會馬上完成大量的 工作 - 如檢查更新,正在下載稍后流程中使用的內(nèi)容,或執(zhí)行大型的磁盤I/O 操作。

讓我們把Visual Studio 代碼作為一個例子。 當(dāng)你打開一個文件,它會立刻展示沒有高亮任何代碼的內(nèi)容,優(yōu)先實現(xiàn)和文本交互的功能。 一旦它完成了這項工作,它將繼續(xù)讓代碼高亮。

怎么做?

讓我們考慮一個示例,并假定您的應(yīng)用程序正在以架空的.foo形式解析文件 。 為了做到這一點,它依賴同樣架空的foo-parserver 模塊。 在傳統(tǒng)的 Node.js 開發(fā)中,你可以寫代碼熱加載依賴:

const fs = require('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 }

在上面的例子中,我們做了很多工作,一旦文件加載,我們就會立即執(zhí)行。 我們需要立即獲取解析的文件嗎? 或許我們可以晚一點再做這件事,當(dāng)getParsedFiles() 真正的執(zhí)行到的時候?

// "fs" is likely already being loaded, so the `require()` call is cheap
const fs = require('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.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 }

簡而言之,只有當(dāng)需要的時候才分配資源,而不是在你的應(yīng)用啟動時分配所有。

3. 阻塞主進程

Electron的主要進程(有時稱為“瀏覽器進程”) 非常特殊:它是與你應(yīng)用的所有其他進程的父進程,也是和操作系統(tǒng)交互的關(guān)鍵進程。它處理窗口、交互以及應(yīng)用程序內(nèi)各個組件之間的通信。它還包含 UI 線程。

在任何情況下你都不應(yīng)阻塞此進程或者運行時間長的用戶界面線程。 阻塞UI線程意味著您的整個應(yīng)用程序?qū)鼋Y(jié)直到主進程準(zhǔn)備好繼續(xù)處理。

為什么??

主進程及其 UI 線程本質(zhì)上是應(yīng)用程序內(nèi)部主要操作的控制塔。當(dāng)操作系統(tǒng)告訴您的應(yīng)用程序有關(guān)鼠標(biāo)單擊的信息時,它會在到達您的窗口之前完成主進程。如果您的窗口呈現(xiàn)黃色平滑動畫, 它需要和 GPU 進程進行通信——再次穿越主進程。

Electron 和 Chromium 謹慎地將大型的磁盤I/O 和 CPU綁定的操作放入新線程,以避免阻塞UI 線程。 你也應(yīng)該這樣做。

怎么做??

Electron強大的多進程架構(gòu)隨時準(zhǔn)備幫助你完成你的長期任務(wù),但其中也包含少量性能陷阱。

  1. 對于需要長期占用 CPU 的繁重任務(wù),利用worker threads,請考慮將它們移動到 BrowserWindow,或(作為最后手段)生成一個專用進程。

  2. 盡可能避免使用同步 IPC 和 @electron/remote 模塊。 雖然有合法的使用案例,但很容易不知情地阻塞 UI 線程。

  3. 避免在主進程中使用阻塞 I/O 操作。 簡而言之,每當(dāng)Node.js的核心模塊 (如fschild_process) 提供一個同步版本或 異步版本,你更應(yīng)該使用異步和非阻塞式的變量。

4. 阻塞渲染進程

自從 Electron 使用了當(dāng)前版本的 Chrome,你可以使用Web 平臺提供的最新和最優(yōu)秀的功能來推遲或卸載繁重的操作,以使你的應(yīng)用保持流暢和迅速的反應(yīng)。

為什么??

你的應(yīng)用可能有很多JavaScript在渲染過程中運行。 有個技巧是盡快執(zhí)行操作,而不占用保持滾動平滑、響應(yīng)用戶輸入或60幀/秒動畫所需的資源。

如果有用戶抱怨你的應(yīng)用“口吃”的時候在渲染的代碼中編排操作流就顯得尤其重要。

怎么做?

一般來說,所有用于構(gòu)建現(xiàn)代瀏覽器的性能網(wǎng)絡(luò)應(yīng)用程序的建議,對于Electron 的渲染器也同樣適用。 現(xiàn)在處理你的應(yīng)用的主要兩個方法是對于小的操作使用requestIdleCallback() 而長時間運行的操作使用 Web Workers。

requestIdleCallback()允許開發(fā)者將函數(shù)排隊為在進程進入空閑期后立刻執(zhí)行。 它使你能夠在不影響用戶體驗的情況下執(zhí)行低優(yōu)先級或后臺執(zhí)行的工作。 想要了解如何使用它的更多信息,請查看MDN上的文檔。

Web Workers是在單獨線程上運行代碼的一個好方式。 有一些注意事項需要考慮 - 請查閱 Electron 的 多線程文檔 和 MDN 的 Web Workers文檔。 對于長時間并且大量使用CPU的操作來說它們是一個理想的解析器。

5. 不必要的polyfills?

Electron的一大好處是,你準(zhǔn)確地知道哪個引擎將解析你的 JavaScript, HTML和CSS。 如果你重新設(shè)計的代碼是為整個網(wǎng)頁編寫的,請確保不會polyfill包含在Electron 中的特性。

為什么??

現(xiàn)在互聯(lián)網(wǎng)構(gòu)建網(wǎng)頁應(yīng)用程序時,最老的環(huán)境決定了你能夠和不能使用的功能。 盡管Electron支持性能良好的 CSS 選擇器和動畫,但是較早的瀏覽器可能不支持。 在你可以使用WebGL的場合,你的開發(fā)者可能選擇了一個資源更加匱乏的解決方案來支持舊機器。

當(dāng)它遇到JavaScript時, 你可能已經(jīng)包含了工具包庫,如DOM選擇器 jQuery 或是 如regenerator-runtime支持async/await 的polyfills。

基于 JavaScript 的polyfill速度比Electron 中的原生特征要快一些。 不要通過發(fā)布你自己的網(wǎng)絡(luò)平臺標(biāo)準(zhǔn)來減慢你的 Electron 應(yīng)用速度。

怎么做?

假定當(dāng)前版本的 Electron不需要使用polyfills。 如果你有所疑慮,檢查 caniuse.com 以確認 是否 在你的Electron版本中使用的Chromium版本 已經(jīng)支持了你需要的特性.

此外,仔細檢查您使用的三方庫。 它們是否真的必要? 例如,jQuery非常成功,它的許多功能現(xiàn)在都是 標(biāo)準(zhǔn)JavaScript功能設(shè)置的 的一部分。

如果您正在使用 TypeScript 這樣的編譯器,檢查它的配置并確保你的目標(biāo)是Electron 支持的最新 ECMAScript 版本。

6. 不必要的或者阻塞的網(wǎng)絡(luò)請求?

避免從互聯(lián)網(wǎng)中獲取幾乎不變化的資源,如果它可以輕松地與你的應(yīng)用程序捆綁起來。

為什么?

許多開始使用基于Web的應(yīng)用程序的Electron用戶后來都使用了桌面應(yīng)用。 作為網(wǎng)頁開發(fā)者,我們習(xí)慣了從各種內(nèi)容交付網(wǎng)站加載資源。既然您正在發(fā)布一個合適的桌面應(yīng)用程序,請盡可能嘗試“切斷電源”并避免讓您的用戶等待永遠不會更改并且可以輕松包含在您的應(yīng)用程序中的資源。

一個典型的例子是谷歌字體。 許多開發(fā)者使用谷歌令人印象深刻的免費字體集,這些字體通過內(nèi)容交付網(wǎng)絡(luò)獲取。 方法顯而易見:包括幾行CSS 和谷歌將處理其余部分。

構(gòu)建Electron應(yīng)用程序時,如果你下載字體并將其包含在應(yīng)用包中,你的用戶將會得到更好的服務(wù)。

怎么做?

在理想情況下,你的應(yīng)用程序不需要網(wǎng)絡(luò)就可以運行。 要達到這個目標(biāo),你必須了解你的應(yīng)用正在下載哪些資源以及這些資源的大小。

要做到這一點,請打開開發(fā)者工具。 導(dǎo)航到 Network 選項卡,然后檢查 Disable cache 選項。 然后重新加載你的頁面。 除非你的應(yīng)用禁止重新加載, 你通??梢栽谑褂瞄_發(fā)者工具時點擊Cmd + R 或Ctrl + R觸發(fā)重新加載。

開發(fā)者工具將仔細記錄所有網(wǎng)絡(luò)請求。 第一步,評估正在下載的所有資源,首先側(cè)重于較大的文件。 其中是否有任何圖像、字體或媒體文件不會改變并且可以包含在你的包中? 如果可以,把它們打包。

下一步,啟用 Network Throttling。 查找當(dāng)前讀取Online的下拉列表,并選擇較慢的速度,例如Fast 3G。 重新加載你的頁面并查看你的應(yīng)用程序是否有等待任何不必要的資源。 在大多數(shù)情況下,盡管實際上不需要相關(guān)的資源,應(yīng)用還是會等待網(wǎng)絡(luò)請求完成。

作為一個提示, 從互聯(lián)網(wǎng)上加載你可能想要更改的而不發(fā)送應(yīng)用程序更新是一個強有力的策略。 為了進一步控制如何加載資源,請考慮使用Service Worker

7. 打包你的代碼

正如中已經(jīng)指出的那樣,"加載和運行代碼太早", 調(diào)用 require() 是一項繁重的操作。 如果你能夠這樣做,將你的應(yīng)用程序的代碼打包到單個文件中。

為什么?

現(xiàn)代JavaScript開發(fā)通常涉及許多文件和模塊。 對于使用Electron開發(fā)的人來說這是非常好的事情,我們強烈建議你將你的代碼打包到單個文件中以確保調(diào)用require() 時只在你的應(yīng)用加載花費一次開銷。

怎么做??

有許多JavaScript打包的方法可供使用,我們知道我們最好不要通過推薦一種工具來激怒社區(qū)。 然而,我們的確建議您使用一個能夠處理Electron獨特的環(huán)境的打包程序,它需要處理Node.js 和瀏覽器兩種環(huán)境。

在撰寫這篇文章時,受歡迎的選擇包括WebpackParcelrollup.js

8. 不需要默認菜單時調(diào)用 Menu.setApplicationMenu(null)

Electron 將在啟動時使用一些標(biāo)準(zhǔn)條目設(shè)置默認菜單。但是您的應(yīng)用程序可能出于某些原因想要更改它,這將有利于啟動性能。

為什么??

如果您構(gòu)建自己的菜單或使用沒有原生菜單的無框窗口,您應(yīng)該盡早告訴 Electron 不要設(shè)置默認菜單。

怎么做?

在 ?app.on("ready")? 之前調(diào)用 ?Menu.setApplicationMenu(null)?。這將阻止 Electron 設(shè)置默認菜單。有關(guān)相關(guān)討論,另請參閱 https://github.com/electron/electron/issues/35512。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號