跳到主要內容

效能

開發人員經常詢問關於最佳化 Electron 應用程式效能的策略。軟體工程師、消費者和框架開發人員對於「效能」的單一定義並不總是意見一致。本文概述了一些 Electron 維護人員最喜歡的方式,以減少記憶體、CPU 和磁碟資源的使用量,同時確保您的應用程式能快速回應使用者輸入並儘快完成操作。此外,我們希望所有效能策略都能維持您應用程式安全性的高標準。

關於如何使用 JavaScript 建立高效能網站的智慧和資訊,通常也適用於 Electron 應用程式。在某種程度上,討論如何建立高效能 Node.js 應用程式的資源也適用,但請注意理解,「效能」一詞對於 Node.js 後端與在用戶端執行的應用程式而言,意義不同。

此列表為了您的方便而提供,並且非常像我們的安全性檢查清單,並非旨在詳盡無遺。建立一個遵循以下所有步驟的緩慢 Electron 應用程式可能是可能的。Electron 是一個強大的開發平台,使您(開發人員)能夠或多或少地做任何您想做的事情。所有這些自由意味著效能很大程度上是您的責任。

衡量、衡量、再衡量

下面的列表包含許多相當直接且易於實作的步驟。但是,要建構應用程式效能最佳的版本,將需要您超越許多步驟。相反,您必須透過仔細的效能分析和衡量來仔細檢查應用程式中執行的所有程式碼。瓶頸在哪裡?當使用者點擊按鈕時,哪些操作佔用了大部分時間?當應用程式只是閒置時,哪些物件佔用了最多的記憶體?

我們一次又一次地看到,建構高效能 Electron 應用程式最成功的策略是分析正在執行的程式碼,找出其中最耗費資源的部分,並對其進行最佳化。一遍又一遍地重複這個看似費力的過程將顯著提高您應用程式的效能。來自使用 Visual Studio Code 或 Slack 等主要應用程式的經驗表明,這種實務是迄今為止提高效能最可靠的策略。

若要進一步了解如何分析應用程式的程式碼,請熟悉 Chrome 開發人員工具。對於一次查看多個處理程序的進階分析,請考慮Chrome Tracing工具。

檢查清單:效能建議

如果您嘗試這些步驟,您的應用程式很可能可以更精簡、更快,並且通常更節省資源。

  1. 隨意包含模組
  2. 過早載入和執行程式碼
  3. 封鎖主要處理程序
  4. 封鎖渲染器處理程序
  5. 不必要的 Polyfill
  6. 不必要或封鎖的網路請求
  7. 捆綁您的程式碼
  8. 當您不需要預設選單時,呼叫 Menu.setApplicationMenu(null)

1. 隨意包含模組

在將 Node.js 模組新增到您的應用程式之前,請檢查該模組。該模組包含多少個依賴項?僅在 require() 陳述式中呼叫它需要什麼樣的資源?您可能會發現,在 NPM 套件註冊表上下載次數最多或 GitHub 上星數最多的模組,實際上並不是最精簡或最小的模組。

為什麼?

此建議背後的理由最好用真實世界的範例來說明。在 Electron 的早期,可靠地偵測網路連線能力是一個問題,導致許多應用程式使用一個公開簡單 isOnline() 方法的模組。

該模組透過嘗試連線到許多知名的端點來偵測您的網路連線能力。對於這些端點的列表,它依賴於另一個模組,該模組也包含一個知名埠號的列表。這個依賴項本身又依賴於一個包含埠號資訊的模組,該模組以 JSON 檔案的形式提供,其中包含超過 100,000 行的內容。每當模組被載入時(通常在 require('module') 陳述式中),它都會載入其所有依賴項,並最終讀取和解析此 JSON 檔案。解析數千行 JSON 是一個非常昂貴的操作。在速度較慢的機器上,它可能需要數秒的時間。

在許多伺服器環境中,啟動時間實際上是無關緊要的。如果 Node.js 伺服器在伺服器啟動時將所有需要的資訊載入記憶體中,以加快服務請求的速度,則 Node.js 伺服器很可能實際上「效能更高」。本範例中討論的模組並不是「壞」模組。但是,Electron 應用程式不應載入、解析和儲存在記憶體中它實際上不需要的資訊。

簡而言之,主要為執行 Linux 的 Node.js 伺服器編寫的看似優秀的模組,對於您應用程式的效能來說可能是壞消息。在這個特定的範例中,正確的解決方案是完全不使用模組,而是使用 Chromium 後續版本中包含的連線檢查。

如何做?

在考慮模組時,我們建議您檢查

  1. 包含的依賴項的大小
  2. 載入 (require()) 它所需的資源
  3. 執行您感興趣的操作所需的資源

可以使用命令列上的單一命令來產生載入模組的 CPU 效能分析和堆積記憶體效能分析。在下面的範例中,我們正在查看熱門模組 request

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

執行此命令會在您執行它的目錄中產生一個 .cpuprofile 檔案和一個 .heapprofile 檔案。這兩個檔案都可以使用 Chrome 開發人員工具進行分析,分別使用 PerformanceMemory 標籤。

Performance CPU Profile

Performance Heap Memory Profile

在本範例中,在作者的機器上,我們看到載入 request 花費了將近半秒的時間,而 node-fetch 佔用的記憶體明顯更少,並且少於 50 毫秒。

2. 過早載入和執行程式碼

如果您有昂貴的設定操作,請考慮延遲這些操作。檢查應用程式啟動後立即執行的所有工作。不要立即啟動所有操作,而是考慮按照更符合使用者旅程的順序錯開它們。

在傳統的 Node.js 開發中,我們習慣於將所有 require() 陳述式放在頂部。如果您目前正在使用相同的策略編寫您的 Electron 應用程式,並且正在使用您不需要立即使用的大型模組,請應用相同的策略並將載入延遲到更合適的時間。

為什麼?

載入模組是一個非常昂貴的操作,尤其是在 Windows 上。當您的應用程式啟動時,不應讓使用者等待目前不必要的操作。

這似乎很明顯,但許多應用程式傾向於在應用程式啟動後立即執行大量工作,例如檢查更新、下載稍後流程中使用的內容或執行繁重的磁碟 I/O 操作。

讓我們以 Visual Studio Code 為例。當您開啟檔案時,它會立即向您顯示檔案,而不會進行任何程式碼醒目提示,優先考慮您與文字互動的能力。完成這項工作後,它將繼續進行程式碼醒目提示。

如何做?

讓我們考慮一個範例,並假設您的應用程式正在解析虛構的 .foo 格式的檔案。為了做到這一點,它依賴於同樣虛構的 foo-parser 模組。在傳統的 Node.js 開發中,您可能會編寫急切載入依賴項的程式碼

parser.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() 時執行此工作嗎?

parser.js
// "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 執行緒。封鎖 UI 執行緒意味著您的整個應用程式將凍結,直到主要處理程序準備好繼續處理。

為什麼?

主要處理程序及其 UI 執行緒本質上是應用程式內部主要操作的控制塔。當作業系統告知您的應用程式有關滑鼠點擊時,它會先通過主要處理程序,然後才到達您的視窗。如果您的視窗正在呈現流暢的動畫,它將需要與 GPU 處理程序討論該問題,再次通過主要處理程序。

Electron 和 Chromium 會小心地將繁重的磁碟 I/O 和 CPU 密集型操作放到新的執行緒上,以避免封鎖 UI 執行緒。您也應該這樣做。

如何做?

Electron 強大的多處理程序架構隨時準備協助您處理長時間執行的任務,但也包含少量效能陷阱。

  1. 對於長時間執行的 CPU 密集型任務,請使用worker thread,考慮將它們移動到 BrowserWindow,或者(作為最後手段)產生專用處理程序。

  2. 盡可能避免使用同步 IPC 和 @electron/remote 模組。雖然存在合法的用例,但不知不覺地封鎖 UI 執行緒太容易了。

  3. 避免在主要處理程序中使用封鎖 I/O 操作。簡而言之,每當核心 Node.js 模組(如 fschild_process)提供同步或非同步版本時,您都應該優先選擇非同步和非封鎖變體。

4. 封鎖渲染器處理程序

由於 Electron 隨附了最新版本的 Chrome,因此您可以利用 Web Platform 提供的最新和最棒的功能,以延遲或卸載繁重的操作,從而保持應用程式的流暢和快速回應。

為什麼?

您的應用程式可能有很多 JavaScript 需要在渲染器處理程序中執行。訣竅是以盡可能快的速度執行操作,而不會佔用保持滾動流暢、回應使用者輸入或 60fps 動畫所需的資源。

如果使用者抱怨您的應用程式有時會「卡頓」,那麼協調渲染器程式碼中的操作流程尤其有用。

如何做?

一般來說,為現代瀏覽器建構高效能 Web 應用程式的所有建議也適用於 Electron 的渲染器。您目前可用的兩個主要工具是適用於小型操作的 requestIdleCallback() 和適用於長時間執行操作的 Web Workers

requestIdleCallback() 允許開發人員將函數排入佇列,以便在處理程序進入閒置期時立即執行。它使您能夠執行低優先順序或背景工作,而不會影響使用者體驗。有關如何使用它的更多資訊,請查看 MDN 上的文件

Web Workers 是一個在單獨的執行緒上執行程式碼的強大工具。有一些注意事項需要考慮 – 請查閱 Electron 的多執行緒文件MDN Web Workers 文件。它們是任何需要大量 CPU 功率長時間運作的操作的理想解決方案。

5. 不必要的 Polyfill

Electron 的一大優點是您確切知道哪個引擎將解析您的 JavaScript、HTML 和 CSS。如果您要重新利用為廣泛的 Web 編寫的程式碼,請確保不要對 Electron 中包含的功能進行 Polyfill。

為什麼?

當為當今的網際網路建構 Web 應用程式時,最舊的環境決定了您可以使用和不能使用哪些功能。即使 Electron 支援效能良好的 CSS 濾鏡和動畫,較舊的瀏覽器可能不支援。在您可以使用 WebGL 的地方,您的開發人員可能選擇了更耗費資源的解決方案來支援較舊的手機。

在 JavaScript 方面,您可能包含了工具組函式庫,例如用於 DOM 選取器的 jQuery 或用於支援 async/await 的 Polyfill,例如 regenerator-runtime

基於 JavaScript 的 Polyfill 比 Electron 中等效的原生功能更快的情況很少見。不要透過發布您自己版本的標準 Web 平台功能來減慢您的 Electron 應用程式速度。

如何做?

在目前的 Electron 版本中,假設 Polyfill 是不必要的。如果您有疑問,請查看 caniuse.com 並檢查您的 Electron 版本中使用的 Chromium 版本是否支援您想要的功能。

此外,仔細檢查您使用的函式庫。它們真的有必要嗎?例如,jQuery 非常成功,以至於它的許多功能現在已成為可用的標準 JavaScript 功能集的一部分。

如果您正在使用轉譯器/編譯器(例如 TypeScript),請檢查其設定,並確保您的目標是 Electron 支援的最新 ECMAScript 版本。

6. 不必要或封鎖的網路請求

如果很少變更的資源可以輕鬆地與您的應用程式捆綁在一起,請避免從網際網路擷取它們。

為什麼?

許多 Electron 使用者從完全基於 Web 的應用程式開始,他們正在將其轉變為桌面應用程式。作為 Web 開發人員,我們習慣於從各種內容傳遞網路載入資源。既然您正在發布適當的桌面應用程式,請嘗試在可能的情況下「切斷連線」,並避免讓您的使用者等待永遠不會變更且可以輕鬆包含在您的應用程式中的資源。

一個典型的例子是 Google Fonts。許多開發人員使用 Google 令人印象深刻的免費字型集合,其中附帶內容傳遞網路。重點很簡單:包含幾行 CSS,Google 將處理其餘部分。

在建構 Electron 應用程式時,如果您下載字型並將它們包含在您的應用程式捆綁包中,您的使用者會獲得更好的服務。

如何做?

在理想的世界中,您的應用程式根本不需要網路即可運作。要達到這個目標,您必須了解您的應用程式正在下載哪些資源,以及這些資源有多大。

為此,請開啟開發人員工具。導航到 Network 標籤並選取 Disable cache 選項。然後,重新載入您的渲染器。除非您的應用程式禁止此類重新載入,否則您通常可以透過在開發人員工具處於焦點狀態時按下 Cmd + RCtrl + R 來觸發重新載入。

這些工具現在將仔細記錄所有網路請求。在第一遍中,盤點所有正在下載的資源,首先關注較大的檔案。其中是否有任何不變更且可以包含在您的捆綁包中的圖像、字型或媒體檔案?如果是,請包含它們。

下一步,啟用 Network Throttling。找到目前顯示 Online 的下拉式選單,然後選取較慢的速度,例如 Fast 3G。重新載入您的渲染器,看看是否有任何您的應用程式不必要地等待的資源。在許多情況下,應用程式將等待網路請求完成,儘管實際上並不需要所涉及的資源。

作為提示,從網際網路載入您可能想要變更而無需發布應用程式更新的資源是一個強大的策略。為了更進階地控制資源的載入方式,請考慮投資Service Workers

7. 捆綁您的程式碼

正如在「過早載入和執行程式碼」中已經指出的那樣,呼叫 require() 是一個昂貴的操作。如果可以做到,請將應用程式的程式碼捆綁到單一檔案中。

為什麼?

現代 JavaScript 開發通常涉及許多檔案和模組。雖然這對於使用 Electron 開發來說完全沒問題,但我們強烈建議您將所有程式碼捆綁到一個單一檔案中,以確保呼叫 require() 中包含的額外負荷僅在您的應用程式載入時支付一次。

如何做?

市面上有許多 JavaScript 捆綁器,我們很清楚不適合透過推薦一個工具而不是另一個工具來激怒社群。但是,我們確實建議您使用能夠處理 Electron 獨特環境的捆綁器,該環境需要處理 Node.js 和瀏覽器環境。

在撰寫本文時,熱門的選擇包括 WebpackParcelrollup.js

8. 當您不需要預設選單時,呼叫 Menu.setApplicationMenu(null)

Electron 將在啟動時設定一個包含一些標準條目的預設選單。但是您的應用程式可能有理由想要更改它,這將有益於啟動效能。

為什麼?

如果您建構自己的選單或使用沒有原生選單的無框視窗,您應該盡早告訴 Electron 不要設定預設選單。

如何做?

app.on("ready") 之前呼叫 Menu.setApplicationMenu(null)。這將阻止 Electron 設定預設選單。另請參閱 https://github.com/electron/electron/issues/35512 以進行相關討論。