メインコンテンツへスキップ

パフォーマンス

開発者は、Electron アプリケーションのパフォーマンスを最適化するための戦略についてよく質問します。ソフトウェアエンジニア、消費者、フレームワーク開発者は、常に「パフォーマンス」の意味について単一の定義に合意するわけではありません。このドキュメントでは、Electron のメンテナーが好んで使用する、メモリ、CPU、ディスクリソースの使用量を削減する方法について概説します。同時に、アプリがユーザーの入力に迅速に対応し、可能な限り迅速に操作を完了できるようにします。さらに、すべてのパフォーマンス戦略がアプリのセキュリティに対して高い基準を維持することを望んでいます。

JavaScript を使用してパフォーマンスの高いウェブサイトを構築する方法に関する知恵と情報は、一般的に Electron アプリにも適用できます。ある程度まで、パフォーマンスの高い Node.js アプリケーションを構築する方法について議論するリソースも適用できますが、「パフォーマンス」という用語が、Node.js バックエンドとクライアントで実行されるアプリケーションとでは意味が異なることを理解するように注意してください。

このリストは便宜のために提供されており、私たちのセキュリティチェックリストと同様に、網羅的なものではありません。以下に概説するすべての手順に従ったとしても、低速な Electron アプリを構築することは可能でしょう。Electron は、開発者であるあなたが、多かれ少なかれ好きなことをできるようにする強力な開発プラットフォームです。その自由は、パフォーマンスが主にあなたの責任であることを意味します。

計測、計測、計測

以下のリストには、比較的簡単で実装が容易なステップが多数含まれています。ただし、アプリの最もパフォーマンスの高いバージョンを構築するには、多くのステップを超えて進む必要があります。代わりに、プロファイリングと測定を慎重に行うことで、アプリで実行されているすべてのコードを綿密に調べる必要があります。ボトルネックはどこにありますか?ユーザーがボタンをクリックしたとき、どの操作に大部分の時間が費やされていますか?アプリが単にアイドル状態のとき、どのオブジェクトが最も多くのメモリを消費していますか?

私たちは、パフォーマンスの高い Electron アプリを構築するための最も成功した戦略は、実行中のコードをプロファイリングし、最もリソースを消費する部分を見つけ、それを最適化することであると何度も見てきました。この一見面倒なプロセスを何度も繰り返すことで、アプリのパフォーマンスは劇的に向上します。Visual Studio Code や Slack のような主要なアプリでの経験から、この手法がパフォーマンスを向上させるための最も信頼できる戦略であることが示されています。

アプリのコードをプロファイリングする方法の詳細については、Chrome 開発者ツールをよく理解してください。複数のプロセスを同時に調べる高度な分析については、Chrome Tracingツールを検討してください。

チェックリスト: パフォーマンスに関する推奨事項

これらの手順を試してみると、アプリが少しでもより無駄がなく、高速になり、全体的によりリソースを消費しなくなる可能性があります。

  1. モジュールを不注意に含める
  2. コードのロードと実行が早すぎる
  3. メインプロセスをブロックする
  4. レンダラープロセスをブロックする
  5. 不要なポリフィル
  6. 不要またはブロックするネットワークリクエスト
  7. コードをバンドルする
  8. デフォルトメニューが必要ない場合は、Menu.setApplicationMenu(null)を呼び出す

1. モジュールを不注意に含める

Node.js モジュールをアプリケーションに追加する前に、そのモジュールを調べてください。そのモジュールにはいくつの依存関係が含まれていますか?require()ステートメントで呼び出すだけでどのようなリソースが必要ですか?NPMパッケージレジストリで最もダウンロード数の多いモジュールや、GitHubで最も星の数が多いモジュールが、実際には利用可能な中で最も無駄がなく、最小のものではないことがわかるかもしれません。

なぜ?

この推奨事項の背後にある理由は、実際の例で最もよく説明できます。Electron の初期の頃、ネットワーク接続の信頼性の高い検出が問題となり、多くのアプリが単純なisOnline()メソッドを公開するモジュールを使用していました。

そのモジュールは、いくつかの有名なエンドポイントに到達しようとすることで、ネットワーク接続を検出しました。それらのエンドポイントのリストについては、別のモジュールに依存しており、それには有名なポートのリストも含まれていました。この依存関係自体は、ポートに関する情報を含むモジュールに依存しており、10万行以上のコンテンツを含む JSON ファイルの形式でした。モジュールがロードされるたびに(通常はrequire('module')ステートメントで)、すべての依存関係をロードし、最終的にこの JSON ファイルを読み取り、解析します。数千行の JSON を解析することは非常にコストのかかる操作です。低速なマシンでは、数秒もかかることがあります。

多くのサーバーコンテキストでは、起動時間は事実上無関係です。すべてのポートに関する情報を必要とする Node.js サーバーは、リクエストをより高速に処理するメリットのために、サーバーが起動するたびに必要なすべての情報をメモリにロードする場合、実際には「よりパフォーマンスが高い」可能性が高くなります。この例で説明したモジュールは「悪い」モジュールではありません。ただし、Electron アプリは、実際には必要のない情報をロード、解析、メモリに保存しないでください。

簡単に言うと、主に Linux を実行する Node.js サーバー向けに書かれた一見優れたモジュールは、アプリのパフォーマンスにとっては悪いニュースとなる可能性があります。この特定の例では、正しい解決策はモジュールをまったく使用せず、代わりに Chromium の後のバージョンに含まれている接続チェックを使用することでした。

どうすれば?

モジュールを検討する場合は、以下を確認することをお勧めします。

  1. 含まれる依存関係のサイズ
  2. ロード(require())に必要なリソース
  3. 興味のあるアクションを実行するために必要なリソース

モジュールをロードするための CPU プロファイルとヒープメモリプロファイルは、コマンドラインで1つのコマンドで生成できます。以下の例では、人気のあるモジュールrequestを見ています。

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

このコマンドを実行すると、実行したディレクトリに.cpuprofileファイルと.heapprofileファイルが生成されます。どちらのファイルも、Chrome 開発者ツールを使用して、それぞれパフォーマンスタブとメモリタブを使用して分析できます。

Performance CPU Profile

Performance Heap Memory Profile

この例では、著者のマシンでは、requestのロードにほぼ0.5秒かかりましたが、node-fetchはメモリの使用量が大幅に少なく、50ms未満でした。

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は、UIスレッドのブロッキングを避けるために、負荷の高いディスクI/OとCPUバウンドな操作を新しいスレッドに注意深く配置しています。あなたも同じようにすべきです。

どのようにすればよいでしょうか?

Electronの強力なマルチプロセスアーキテクチャは、長時間実行されるタスクを支援する準備ができていますが、パフォーマンスの落とし穴もいくつか含まれています。

  1. 長時間実行されるCPU負荷の高いタスクには、ワーカー スレッドを利用するか、それらを BrowserWindow に移動することを検討するか、(最後の手段として)専用のプロセスを生成することを検討してください。

  2. 同期IPCと@electron/remoteモジュールの使用はできる限り避けてください。正当なユースケースはありますが、UIスレッドを気付かずにブロックしてしまうことが非常に簡単です。

  3. メインプロセスでブロッキングI/O操作を使用することは避けてください。要するに、コアNode.jsモジュール(fschild_processなど)が同期バージョンと非同期バージョンを提供している場合は、非同期でノンブロッキングなバージョンを優先する必要があります。

4. レンダラープロセスのブロッキング

Electronには最新バージョンのChromeが同梱されているため、Webプラットフォームが提供する最新かつ優れた機能を利用して、アプリをスムーズかつ応答性の高い状態に保つ方法で、負荷の高い操作を延期またはオフロードできます。

なぜでしょうか?

アプリには、おそらくレンダラープロセスで実行する多くのJavaScriptがあるでしょう。重要なのは、スクロールをスムーズに保ち、ユーザー入力に応答し、60fpsでアニメーションを実行するために必要なリソースを奪うことなく、できる限り迅速に操作を実行することです。

ユーザーがアプリが時々「カクつく」と不満を言う場合、レンダラーのコードで操作の流れを調整することが特に役立ちます。

どのようにすればよいでしょうか?

一般的に言って、最新のブラウザ向けにパフォーマンスの高いWebアプリを構築するためのすべてのアドバイスは、Electronのレンダラーにも適用されます。現在、利用できる2つの主要なツールは、小さな操作にはrequestIdleCallback()、長時間の操作にはWeb Workersです。

requestIdleCallback()を使用すると、プロセスがアイドル期間に入るとすぐに実行される関数を開発者がキューに入れることができます。これにより、ユーザーエクスペリエンスに影響を与えることなく、優先度の低い作業やバックグラウンド作業を実行できます。使用方法の詳細については、MDNのドキュメントを確認してください。

Web Workersは、別のスレッドでコードを実行するための強力なツールです。考慮すべきいくつかの注意点があります。Electronのマルチスレッドに関するドキュメントと、Web WorkersのMDNドキュメントを参照してください。これらは、長期間にわたって多くのCPUパワーを必要とするあらゆる操作に理想的なソリューションです。

5. 不要なポリフィル

Electronの大きな利点の1つは、JavaScript、HTML、およびCSSを解析するエンジンを正確に把握できることです。Web全体向けに作成されたコードを再利用している場合は、Electronに含まれる機能をポリフィルしないようにしてください。

なぜでしょうか?

今日のインターネット向けのWebアプリケーションを構築する場合、最も古い環境によって、使用できる機能と使用できない機能が決まります。Electronはパフォーマンスの高いCSSフィルターとアニメーションをサポートしていますが、古いブラウザはサポートしていない可能性があります。WebGLを使用できる場合でも、開発者は古い携帯電話をサポートするために、より多くのリソースを消費するソリューションを選択した可能性があります。

JavaScriptに関しては、DOMセレクターにjQueryのようなツールキットライブラリを含めたり、async/awaitをサポートするためにregenerator-runtimeのようなポリフィルを含めたりした可能性があります。

JavaScriptベースのポリフィルが、Electronの同等のネイティブ機能よりも高速になることはまれです。標準のWebプラットフォーム機能の独自のバージョンを同梱して、Electronアプリの速度を低下させないでください。

どのようにすればよいでしょうか?

現在のバージョンのElectronのポリフィルは不要であるという前提で作業してください。疑問がある場合は、caniuse.comを確認し、Electronバージョンで使用されているChromiumのバージョンが、目的の機能をサポートしているかどうかを確認してください。

さらに、使用するライブラリを注意深く調べてください。それらは本当に必要ですか?たとえば、jQueryは大成功を収めたため、その多くの機能が現在、利用可能な標準のJavaScript機能セットの一部になっています。

TypeScriptのようなトランスパイラー/コンパイラーを使用している場合は、その構成を調べ、Electronでサポートされている最新のECMAScriptバージョンをターゲットにしていることを確認してください。

6. 不要またはブロッキングなネットワークリクエスト

変更がほとんどないリソースを、アプリケーションに簡単にバンドルできる場合は、インターネットからフェッチすることを避けてください。

なぜでしょうか?

多くのElectronユーザーは、デスクトップアプリケーションに変えようとしている、完全にWebベースのアプリから始めます。Web開発者として、さまざまなコンテンツ配信ネットワークからリソースをロードすることに慣れています。適切なデスクトップアプリケーションを同梱しているため、可能な限り「コードを切り」、ユーザーに変更されることのない、アプリに簡単に含めることができるリソースを待たせないようにしてください。

典型的な例は、Googleフォントです。多くの開発者は、コンテンツ配信ネットワークを備えた、Googleの無料フォントの印象的なコレクションを利用しています。その売りは簡単です。数行のCSSを含めるだけで、残りはGoogleが行います。

Electronアプリを構築する場合、フォントをダウンロードしてアプリのバンドルに含める方が、ユーザーにとってより良いサービスになります。

どのようにすればよいでしょうか?

理想的な世界では、アプリケーションはまったくネットワークを必要としないでしょう。そこに到達するには、アプリがダウンロードしているリソースと、それらのリソースのサイズを理解する必要があります。

そのためには、開発者ツールを開きます。ネットワークタブに移動し、キャッシュを無効にするオプションをオンにします。次に、レンダラーをリロードします。アプリがそのようなリロードを禁止していない限り、通常は開発者ツールにフォーカスを当てた状態でCmd + RまたはCtrl + Rを押すことでリロードをトリガーできます。

これで、ツールはすべてのネットワークリクエストを細心の注意を払って記録します。最初に、ダウンロードされているすべてのリソースを把握し、最初に大きなファイルに焦点を当てます。それらの中に、変更がなく、バンドルに含めることができる画像、フォント、またはメディアファイルはありますか?もしそうなら、含めてください。

次のステップとして、ネットワーク スロットリングを有効にします。現在オンラインと表示されているドロップダウンを見つけ、高速3Gなどの遅い速度を選択します。レンダラーをリロードして、アプリが不必要に待機しているリソースがあるかどうかを確認してください。多くの場合、アプリは、実際には関係するリソースを必要としないにもかかわらず、ネットワークリクエストが完了するのを待ちます。

ヒントとして、アプリケーションの更新を配布することなく変更したい可能性のあるリソースをインターネットからロードすることは、強力な戦略です。リソースのロード方法を高度に制御するには、Service Workersへの投資を検討してください。

7. コードをバンドルする

コードのロードと実行が早すぎる」ですでに指摘したように、require()を呼び出すのはコストのかかる操作です。可能であれば、アプリケーションのコードを単一のファイルにバンドルしてください。

なぜでしょうか?

最新のJavaScript開発では、通常、多くのファイルとモジュールが使用されます。Electronで開発する場合、これはまったく問題ありませんが、アプリケーションのロード時にrequire()の呼び出しに含まれるオーバーヘッドが1回だけ支払われるように、すべてのコードを1つのファイルにバンドルすることを強くお勧めします。

どのようにすればよいでしょうか?

JavaScriptのバンドラーはたくさんあり、1つのツールを別のツールよりも推奨してコミュニティを怒らせるようなことはしたくありません。ただし、Node.jsとブラウザー環境の両方を処理する必要があるElectron独自の環境を処理できるバンドラーを使用することをお勧めします。

この記事の執筆時点では、WebpackParcel、およびrollup.jsが一般的な選択肢です。

8. デフォルトメニューが不要な場合はMenu.setApplicationMenu(null)を呼び出す

Electronは、起動時にいくつかの標準エントリを含むデフォルトメニューを設定します。しかし、アプリケーションがそれを変更したい理由があり、それが起動パフォーマンスの向上につながります。

なぜでしょうか?

独自のメニューを作成するか、ネイティブメニューのないフレームレスウィンドウを使用する場合は、デフォルトメニューを設定しないように十分に早くElectronに指示する必要があります。

どのようにすればよいでしょうか?

app.on("ready")の前にMenu.setApplicationMenu(null)を呼び出します。これにより、Electronがデフォルトメニューを設定するのを防ぎます。関連する議論については、https://github.com/electron/electron/issues/35512も参照してください。