Browse Source

docs: Performance checklist (#20757)

* docs: First draft of perf checklist

* docs: More words

* docs: Use standard in code example

* docs: fix broken link

* Update docs/tutorial/performance.md

Co-Authored-By: Charles Kerr <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: Charles Kerr <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: loc <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: loc <[email protected]>

* docs: Implement suggestions

* docs: Include VSCode talk

* chore: Pass linter

* Update docs/tutorial/performance.md

Co-Authored-By: Mark Lee <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: Mark Lee <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: Mark Lee <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: Mark Lee <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: Mark Lee <[email protected]>

* Update docs/tutorial/performance.md

Co-Authored-By: Mark Lee <[email protected]>

* Apply suggestions from code review

Co-Authored-By: Mark Lee <[email protected]>

* Update performance.md

* fix: The process link
trop[bot] 5 years ago
parent
commit
6b70f0e8e6

BIN
docs/images/performance-cpu-prof.png


BIN
docs/images/performance-heap-prof.png


+ 432 - 0
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