diff --git a/docs/images/performance-cpu-prof.png b/docs/images/performance-cpu-prof.png new file mode 100644 index 0000000000..d6e74d90ed Binary files /dev/null and b/docs/images/performance-cpu-prof.png differ diff --git a/docs/images/performance-heap-prof.png b/docs/images/performance-heap-prof.png new file mode 100644 index 0000000000..83772b4444 Binary files /dev/null and b/docs/images/performance-heap-prof.png differ diff --git a/docs/tutorial/performance.md b/docs/tutorial/performance.md new file mode 100644 index 0000000000..9fb1b3c538 --- /dev/null +++ b/docs/tutorial/performance.md @@ -0,0 +1,432 @@ +# Performance + +Developers frequently ask about strategies to optimize the performance of +Electron applications. Software engineers, consumers, and framework developers +do not always agree on one single definition of what "performance" means. This +document outlines some of the Electron maintainers' favorite ways to reduce the +amount of memory, CPU, and disk resources being used while ensuring that your +app is responsive to user input and completes operations as quickly as +possible. Furthermore, we want all performance strategies to maintain a high +standard for your app's security. + +Wisdom and information about how to build performant websites with JavaScript +generally applies to Electron apps, too. To a certain extent, resources +discussing how to build performant Node.js applications also apply, but be +careful to understand that the term "performance" means different things for +a Node.js backend than it does for an application running on a client. + +This list is provided for your convenience – and is, much like our +[security checklist][security] – not meant to exhaustive. It is probably possible +to build a slow Electron app that follows all the steps outlined below. Electron +is a powerful development platform that enables you, the developer, to do more +or less whatever you want. All that freedom means that performance is largely +your responsibility. + +## Measure, Measure, Measure + +The list below contains a number of steps that are fairly straightforward and +easy to implement. However, building the most performant version of your app +will require you to go beyond a number of steps. Instead, you will have to +closely examine all the code running in your app by carefully profiling and +measuring. Where are the bottlenecks? When the user clicks a button, what +operations take up the brunt of the time? While the app is simply idling, which +objects take up the most memory? + +Time and time again, we have seen that the most successful strategy for building +a performant Electron app is to profile the running code, find the most +resource-hungry piece of it, and to optimize it. Repeating this seemingly +laborious process over and over again will dramatically increase your app's +performance. Experience from working with major apps like Visual Studio Code or +Slack has shown that this practice is by far the most reliable strategy to +improve performance. + +To learn more about how to profile your app's code, familiarize yourself with +the Chrome Developer Tools. For advanced analysis looking at multiple processes +at once, consider the [Chrome Tracing] tool. + +### Recommended Reading + + * [Get Started With Analyzing Runtime Performance][chrome-devtools-tutorial] + * [Talk: "Visual Studio Code - The First Second"][vscode-first-second] + +## Checklist + +Chances are that your app could be a little leaner, faster, and generally less +resource-hungry if you attempt these steps. + +1) [Carelessly including modules](#1-carelessly-including-modules) +2) [Loading and running code too soon](#2-loading-and-running-code-too-soon) +3) [Blocking the main process](#3-blocking-the-main-process) +4) [Blocking the renderer process](#4-blocking-the-renderer-process) +5) [Unnecessary polyfills](#5-unnecessary-polyfills) +6) [Unnecessary or blocking network requests](#6-unnecessary-or-blocking-network-requests) +7) [Bundle your code](#7-bundle-your-code) + +## 1) Carelessly including modules + +Before adding a Node.js module to your application, examine said module. How +many dependencies does that module include? What kind of resources does +it need to simply be called in a `require()` statement? You might find +that the module with the most downloads on the NPM package registry or the most stars on GitHub +is not in fact the leanest or smallest one available. + +### Why? + +The reasoning behind this recommendation is best illustrated with a real-world +example. During the early days of Electron, reliable detection of network +connectivity was a problem, resulting many apps to use a module that exposed a +simple `isOnline()` method. + +That module detected your network connectivity by attempting to reach out to a +number of well-known endpoints. For the list of those endpoints, it depended on +a different module, which also contained a list of well-known ports. This +dependency itself relied on a module containing information about ports, which +came in the form of a JSON file with more than 100,000 lines of content. +Whenever the module was loaded (usually in a `require('module')` statement), +it would load all its dependencies and eventually read and parse this JSON +file. Parsing many thousands lines of JSON is a very expensive operation. On +a slow machine it can take up whole seconds of time. + +In many server contexts, startup time is virtually irrelevant. A Node.js server +that requires information about all ports is likely actually "more performant" +if it loads all required information into memory whenever the server boots at +the benefit of serving requests faster. The module discussed in this example is +not a "bad" module. Electron apps, however, should not be loading, parsing, and +storing in memory information that it does not actually need. + +In short, a seemingly excellent module written primarily for Node.js servers +running Linux might be bad news for your app's performance. In this particular +example, the correct solution was to use no module at all, and to instead use +connectivity checks included in later versions of Chromium. + +### How? + +When considering a module, we recommend that you check: + +1. the size of dependencies included +2) the resources required to load (`require()`) it +3. the resources required to perform the action you're interested in + +Generating a CPU profile and a heap memory profile for loading a module can be done +with a single command on the command line. In the example below, we're looking at +the popular module `request`. + +```sh +node --cpu-prof --heap-prof -e "require('request')" +``` + +Executing this command results in a `.cpuprofile` file and a `.heapprofile` +file in the directory you executed it in. Both files can be analyzed using +the Chrome Developer Tools, using the `Performance` and `Memory` tabs +respectively. + +![performance-cpu-prof] + +![performance-heap-prof] + +In this example, on the author's machine, we saw that loading `request` took +almost half a second, whereas `node-fetch` took dramatically less memory +and less than 50ms. + +## 2) Loading and running code too soon + +If you have expensive setup operations, consider deferring those. Inspect all +the work being executed right after the application starts. Instead of firing +off all operations right away, consider staggering them in a sequence more +closely aligned with the user's journey. + +In traditional Node.js development, we're used to putting all our `require()` +statements at the top. If you're currently writing your Electron application +using the same strategy _and_ are using sizable modules that you do not +immediately need, apply the same strategy and defer loading to a more +opportune time. + +### Why? + +Loading modules is a surprisingly expensive operation, especially on Windows. +When your app starts, it should not make users wait for operations that are +currently not necessary. + +This might seem obvious, but many applications tend to do a large amount of +work immediately after the app has launched - like checking for updates, +downloading content used in a later flow, or performing heavy disk I/O +operations. + +Let's consider Visual Studio Code as an example. When you open a file, it will +immediately display the file to you without any code highlighting, prioritizing +your ability to interact with the text. Once it has done that work, it will +move on to code highlighting. + +### How? + +Let's consider an example and assume that your application is parsing files +in the fictitious `.foo` format. In order to do that, it relies on the +equally fictitious `foo-parser` module. In traditional Node.js development, +you might write code that eagerly loads dependencies: + +```js +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 } +``` + +In the above example, we're doing a lot of work that's being executed as soon +as the file is loaded. Do we need to get parsed files right away? Could we +do this work a little later, when `getParsedFiles()` is actually called? + +```js +// "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 } +``` + +In short, allocate resources "just in time" rather than allocating them all +when your app starts. + +## 3) Blocking the main process + +Electron's main process (sometimes called "browser process") is special: It is +the parent process to all your app's other processes and the primary process +the operating system interacts with. It handles windows, interactions, and the +communication between various components inside your app. It also houses the +UI thread. + +Under no circumstances should you block this process and the UI thread with +long-running operations. Blocking the UI thread means that your entire app +will freeze until the main process is ready to continue processing. + +### Why? + +The main process and its UI thread are essentially the control tower for major +operations inside your app. When the operating system tells your app about a +mouse click, it'll go through the main process before it reaches your window. +If your window is rendering a buttery-smooth animation, it'll need to talk to +the GPU process about that – once again going through the main process. + +Electron and Chromium are careful to put heavy disk I/O and CPU-bound operations +onto new threads to avoid blocking the UI thread. You should do the same. + +### How? + +Electron's powerful multi-process architecture stands ready to assist you with +your long-running tasks, but also includes a small number of performance traps. + +1) For long running CPU-heavy tasks, make use of +[worker threads][worker-threads], consider moving them to the BrowserWindow, or +(as a last resort) spawn a dedicated process. + +2) Avoid using the synchronous IPC and the `remote` module as much as possible. +While there are legitimate use cases, it is far too easy to unknowingly block +the UI thread using the `remote` module. + +3) Avoid using blocking I/O operations in the main process. In short, whenever +core Node.js modules (like `fs` or `child_process`) offer a synchronous or an +asynchronous version, you should prefer the asynchronous and non-blocking +variant. + + +## 4) Blocking the renderer process + +Since Electron ships with a current version of Chrome, you can make use of the +latest and greatest features the Web Platform offers to defer or offload heavy +operations in a way that keeps your app smooth and responsive. + +### Why? + +Your app probably has a lot of JavaScript to run in the renderer process. The +trick is to execute operations as quickly as possible without taking away +resources needed to keep scrolling smooth, respond to user input, or animations +at 60fps. + +Orchestrating the flow of operations in your renderer's code is +particularly useful if users complain about your app sometimes "stuttering". + +### How? + +Generally speaking, all advice for building performant web apps for modern +browsers apply to Electron's renderers, too. The two primary tools at your +disposal are currently `requestIdleCallback()` for small operations and +`Web Workers` for long-running operations. + +*`requestIdleCallback()`* allows developers to queue up a function to be +executed as soon as the process is entering an idle period. It enables you to +perform low-priority or background work without impacting the user experience. +For more information about how to use it, +[check out its documentation on MDN][request-idle-callback]. + +*Web Workers* are a powerful tool to run code on a separate thread. There are +some caveats to consider – consult Electron's +[multithreading documentation][multithreading] and the +[MDN documentation for Web Workers][web-workers]. They're an ideal solution +for any operation that requires a lot of CPU power for an extended period of +time. + + +## 5) Unnecessary polyfills + +One of Electron's great benefits is that you know exactly which engine will +parse your JavaScript, HTML, and CSS. If you're re-purposing code that was +written for the web at large, make sure to not polyfill features included in +Electron. + +### Why? + +When building a web application for today's Internet, the oldest environments +dictate what features you can and cannot use. Even though Electron supports +well-performing CSS filters and animations, an older browser might not. Where +you could use WebGL, your developers may have chosen a more resource-hungry +solution to support older phones. + +When it comes to JavaScript, you may have included toolkit libraries like +jQuery for DOM selectors or polyfills like the `regenerator-runtime` to support +`async/await`. + +It is rare for a JavaScript-based polyfill to be faster than the equivalent +native feature in Electron. Do not slow down your Electron app by shipping your +own version of standard web platform features. + +### How? + +Operate under the assumption that polyfills in current versions of Electron +are unnecessary. If you have doubts, check [caniuse.com][https://caniuse.com/] +and check if the [version of Chromium used in your Electron version](../api/process.md#processversionschrome-readonly) +supports the feature you desire. + +In addition, carefully examine the libraries you use. Are they really necessary? +`jQuery`, for example, was such a success that many of its features are now part +of the [standard JavaScript feature set available][jquery-need]. + +If you're using a transpiler/compiler like TypeScript, examine its configuration +and ensure that you're targeting the latest ECMAScript version supported by +Electron. + + +## 6) Unnecessary or blocking network requests + +Avoid fetching rarely changing resources from the internet if they could easily +be bundled with your application. + +### Why? + +Many users of Electron start with an entirely web-based app that they're +turning into a desktop application. As web developers, we are used to loading +resources from a variety of content delivery networks. Now that you are +shipping a proper desktop application, attempt to "cut the cord" where possible + - and avoid letting your users wait for resources that never change and could +easily be included in your app. + +A typical example is Google Fonts. Many developers make use of Google's +impressive collection of free fonts, which comes with a content delivery +network. The pitch is straightforward: Include a few lines of CSS and Google +will take care of the rest. + +When building an Electron app, your users are better served if you download +the fonts and include them in your app's bundle. + +### How? + +In an ideal world, your application wouldn't need the network to operate at +all. To get there, you must understand what resources your app is downloading +\- and how large those resources are. + +To do so, open up the developer tools. Navigate to the `Network` tab and check +the `Disable cache` option. Then, reload your renderer. Unless your app +prohibits such reloads, you can usually trigger a reload by hitting `Cmd + R` +or `Ctrl + R` with the developer tools in focus. + +The tools will now meticulously record all network requests. In a first pass, +take stock of all the resources being downloaded, focusing on the larger files +first. Are any of them images, fonts, or media files that don't change and +could be included with your bundle? If so, include them. + +As a next step, enable `Network Throttling`. Find the drop-down that currently +reads `Online` and select a slower speed such as `Fast 3G`. Reload your +renderer and see if there are any resources that your app is unnecessarily +waiting for. In many cases, an app will wait for a network request to complete +despite not actually needing the involved resource. + +As a tip, loading resources from the Internet that you might want to change +without shipping an application update is a powerful strategy. For advanced +control over how resources are being loaded, consider investing in +[Service Workers][service-workers]. + +## 7) Bundle your code + +As already pointed out in +"[Loading and running code too soon](#2-loading-and-running-code-too-soon)", +calling `require()` is an expensive operation. If you are able to do so, +bundle your application's code into a single file. + +### Why? + +Modern JavaScript development usually involves many files and modules. While +that's perfectly fine for developing with Electron, we heavily recommend that +you bundle all your code into one single file to ensure that the overhead +included in calling `require()` is only paid once when your application loads. + +### How? + +There are numerous JavaScript bundlers out there and we know better than to +anger the community by recommending one tool over another. We do however +recommend that you use a bundler that is able to handle Electron's unique +environment that needs to handle both Node.js and browser environments. + +As of writing this article, the popular choices include [Webpack][webpack], +[Parcel][parcel], and [rollup.js][rollup]. + +[security]: ./security.md +[performance-cpu-prof]: ../images/performance-cpu-prof.png +[performance-heap-prof]: ../images/performance-heap-prof.png +[chrome-devtools-tutorial]: https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/ +[chrome-tracing-tutorial]: +[worker-threads]: https://nodejs.org/api/worker_threads.html +[web-workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers +[request-idle-callback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback +[multithreading]: ./multithreading.md +[caniuse]: https://caniuse.com/ +[jquery-need]: http://youmightnotneedjquery.com/ +[service-workers]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API +[webpack]: https://webpack.js.org/ +[parcel]: https://parceljs.org/ +[rollup]: https://rollupjs.org/ +[vscode-first-second]: https://www.youtube.com/watch?v=r0OeHRUCCb4