開發(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é)任。
以下列舉了一些直截了當(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)資源不足的情況。
在向你的應(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)考慮一個模塊時,我們建議你做以下檢查:
require()
?) 資源可以使用命令行上的單個命令生成用于加載模塊的 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。
如果你有非常繁重的初始化操作,請考慮推遲進行。 程序啟動立刻查看應(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)用啟動時分配所有。
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ù),但其中也包含少量性能陷阱。
對于需要長期占用 CPU 的繁重任務(wù),利用worker threads,請考慮將它們移動到 BrowserWindow,或(作為最后手段)生成一個專用進程。
盡可能避免使用同步 IPC 和 @electron/remote
模塊。 雖然有合法的使用案例,但很容易不知情地阻塞 UI 線程。
避免在主進程中使用阻塞 I/O 操作。 簡而言之,每當(dāng)Node.js的核心模塊 (如fs
或 child_process
) 提供一個同步版本或 異步版本,你更應(yīng)該使用異步和非阻塞式的變量。
自從 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的操作來說它們是一個理想的解析器。
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 版本。
避免從互聯(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。
正如中已經(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)境。
在撰寫這篇文章時,受歡迎的選擇包括Webpack, Parcel和rollup.js。
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。
更多建議: