How to make your Electron app launch 1,000ms faster
Tags: electron, react, javascript, performance
Published: 2020-08-11 12:31:15

Hi, I'm [Takuya](https://twitter.com/inkdrop_app), an indie developer building a Markdown note-taking app called [Inkdrop](https://www.inkdrop.app/).
This app is built on top of [Electron](https://www.electronjs.org/), a framework that allows you to build a cross-platform desktop app based on NodeJS and Chromium (browser).
It is basically a great framework because you can build desktop apps without learning native frameworks or languages but with JavaScript, HTML and CSS. If you are a web developer, you can build desktop apps quickly.
On the other side, people often mention about the Electron's downside - the app startup time tends to be slow.
My app encountered this issue as well, as I've got complaints about the slow launch speed from some users.
Yeah, the slow startup is so stressful.
But I'm extremely happy that I accomplished to solve it.
The app's TTI (Time to Interactive) has been boosted from 4 seconds to 3 seconds on my mac.
I'd say "1,000msecs faster" instead of just "1sec faster" because it is a significant improvement and I've worked very hard for it!
Take a look at the following comparison screencast:

![Launch speed comparison](https://dev-to-uploads.s3.amazonaws.com/i/j1okykcymr9h7oc64jp2.gif)

You can feel it's quite faster than the previous version.
As you can see above, the app main window shows up a little quicker, and loading the app bundle in the browser window finishes also quickly.
It's currently in beta and the users told me they are happy with the improved launch speed.
I can't wait to roll it out officially.

I guess there are a lot of developers struggling to solve the same issue, so I'd like to share how I've done it.
Let's boost your Electron app!

## TL;DR

* Loading JavaScript is too slow
* Don't call `require()` until you need (300ms improved)
* Use V8 snapshots (700ms improved)

## Loading JavaScript is too slow

So, why do Electron apps tend to start up slowly?
The biggest bottleneck in app launch is obviously the process to load JavaScript.
You can inspect how your app bundle is being loaded in Developer Tools' performance analyzer.

* See also: [Get Started With Analyzing Runtime Performance | Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance)

Press <kbd>Cmd-E</kbd> or the red dot record button to start capturing runtime performance, then reload the app.
And you will see a timeline something like this:

![Performance analysis](https://dev-to-uploads.s3.amazonaws.com/i/0carwgb6isilf0s4ml44.png)

You should see requiring modules is taking long time in the timeline.
How long it takes depends on how many modules/libraries your app depends.

In my case, my app has an enormous number of dependencies in order to provide its plug-in capability, extensible markdown editor and renderer, and so on.
It seems to be difficult to drop those dependencies for the sake of the launch speed.

If you have a new project, you have to carefully choose libraries for performance.
Less dependencies are always better.

## Don't call `require()` until you need

The first thing you can do to avoid the big loading time is to defer calling `require()` for your dependencies until they're necessary.

My app main window now shows up a little bit faster than the old version.
That's because it was loading `jsdom` in the main process on launch.
I added it to parse HTML but found it is a huge library and it requires several hundreds milliseconds to load.

There are several ways to solve such issue.

### 1. Use a lighter alternative

If you found it heavy to load, you can use a small alternative library if exists.
It turned out that I don't need `jsdom` to parse HTML because there is [`DOMParser`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser) in Web API. You can parse HTML with it like so:

```js
const dom = new DOMParser().parseFromString(html, 'text/html')
```

### 2. Avoid requiring on the evaluating-time

Instead of requiring the library on evaluating your code:

```js
import { JSDOM } from 'jsdom'

export function parseHTML(html) {
  const dom = new JSDOM(html);
  // ...
}
```

Defer requiring it until you actually need the library:

```js
var jsdom = null

function get_jsdom() {
  if (jsdom === null) {
    jsdom = require('jsdom')
  }
  return jsdom
}

export function parseHTML(html) {
  const { JSDOM } = get_jsdom()
  const dom = new JSDOM(html);
  // ...
}
```

It would improve your startup time without dropping the dependency.
Note that you must exclude those dependencies from your app bundle if you are using a module bundler like Webpack.

## Use V8 snapshots

Now my app launches 200-300ms faster, but still loads slow in the renderer process.
The most dependencies can't be deferred to be required as they are used immediately.

Chromium has to read and evaluate your JS and modules which needs long time than you'd imagine even when from local filesystem (1-2 secs in my app).
Most native apps don't need to do that because they are already in binary code and your OS can run them without translating into a machine language.

Chromium's JavaScript engine is v8.
And there is a technique in v8 to speed things up: [V8 snapshots](https://v8.dev/blog/custom-startup-snapshots).
V8 snapshots allow Electron apps to execute some arbitrary JavaScript code and output a binary file containing a serialized heap with all the data that is left in memory after running a GC at the end of the provided script.

[Atom Editor](https://atom.io/) has utilized V8 snapshots and improved startup time 3 years ago:

* [Improving Startup Time | Atom Blog](https://blog.atom.io/2017/04/18/improving-startup-time.html)

Atom team accomplished to boost the startup time around 500ms on their machine.
Looks promising.

### How V8 snapshots work

Let me get straight to the point - It worked great for my app as well.
For example, loading `remark-parse` has been drastically shrunk.

Without v8 snapshots:

![Loading remark-parse without v8 snapshots](https://dev-to-uploads.s3.amazonaws.com/i/58m7lkfoen4k77uh1yue.png)

With v8 snapshots:

![Loading remark-parse with v8 snapshots enabled](https://dev-to-uploads.s3.amazonaws.com/i/hcz49yb502e7hkp5ivv3.png)

Cool!!!

I could improve the loading time on evaluating `browser-main.js` from:

![Loading browser-main.js without v8 snapshots](https://dev-to-uploads.s3.amazonaws.com/i/xfnqga6trzcjpo8aakel.png)
To:

![Loading browser-main.js with v8 snapshots](https://dev-to-uploads.s3.amazonaws.com/i/zffli0yapamv4kcmbmw0.png)

Here is a screencast of loading Preferences window, illustrating how much v8 snapshots improved loading speed of the app bundle:

![Preferences window](https://dev-to-uploads.s3.amazonaws.com/i/ifcm6w6mut3d7hwfaebt.gif)

But how do you load modules from V8 snapshots?
In an Electron app with your custom V8 snapshots, you get `snapshotResult` variable in global scope.
It contains pre-loaded cache data of JavaScript that are already executed beforehand as following:

![snapshotResult](https://dev-to-uploads.s3.amazonaws.com/i/ygjr48x8uh82w1onsndk.png)

You can use those modules without calling `require()`.
That's why V8 snapshots work very fast.

In the next section, I'll explain how to create your custom V8 snapshots.

## How to create custom V8 snapshots

You have to do the following steps:

1. Install tools
2. Preprocess the JavaScript source file with `electron-link`
3. Create the v8 snapshots with `mksnapshot`
4. Load the snapshots in Electron

I created a simple example project for this tutorial. Check out my repository here:

* [inkdropapp/electron-v8snapshots-example: An example for using custom v8 snapshots in an Electron app](https://github.com/inkdropapp/electron-v8snapshots-example)

### Install tools

The following packages are needed:

| package                                                     | description                            |
| ----------------------------------------------------------- | -------------------------------------- |
| [electron](https://github.com/electron/electron)            | Runtime                                |
| [electron-link](https://github.com/atom/electron-link)        | Preprocess the JavaScript source files |
| [electron-mksnapshot](https://github.com/electron/mksnapshot) | Download the `mksnapshot` binaries    |

`mksnapshot` is a tool to create V8 snapshots from your preprocessed JavaScript file with `electron-link`.
`electron-mksnapshot` helps download the compatible `mksnapshot` binaries for Electron.
But if you are using old version of Electron, you have to set `ELECTRON_CUSTOM_VERSION` environment variable to your Electron version:

```sh
# Install mksnapshot for Electron v8.3.0
ELECTRON_CUSTOM_VERSION=8.3.0 npm install
```

Downloading the binaries would take a long time. You can use an Electron mirror by setting `ELECTRON_MIRROR` environment variables as following:

```sh
# Electron mirror for China
ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
```

### Preprocess the JavaScript source file with `electron-link`

`electron-link` helps you generate a JavaScript file which can be snapshotted.
Why you need it is that you can't `require` some modules like NodeJS built-in modules and native modules in a V8 context.
If you have a simple app, you can pass the entry point of your app.
In my case, my app was too complicated to generate a snapshot-able file.
So, I decided to create another JS file for generating the snapshots which just requires some libraries as following:

```js
// snapshot.js
require('react')
require('react-dom')
// ...require more libraries
```

Then, save it as `snapshot.js` in your project directory.
Create the following script that passes the JS file into `electron-link`:

```js
const vm = require('vm')
const path = require('path')
const fs = require('fs')
const electronLink = require('electron-link')

const excludedModules = {}

async function main () {
  const baseDirPath = path.resolve(__dirname, '..')

  console.log('Creating a linked script..')
  const result = await electronLink({
    baseDirPath: baseDirPath,
    mainPath: `${baseDirPath}/snapshot.js`,
    cachePath: `${baseDirPath}/cache`,
    shouldExcludeModule: (modulePath) => excludedModules.hasOwnProperty(modulePath)
  })

  const snapshotScriptPath = `${baseDirPath}/cache/snapshot.js`
  fs.writeFileSync(snapshotScriptPath, result.snapshotScript)

  // Verify if we will be able to use this in `mksnapshot`
  vm.runInNewContext(result.snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true})
}

main().catch(err => console.error(err))
```

It will output a snapshotable script to `<PROJECT_PATH>/cache/snapshot.js`.
This JS file derived from `electron-link` contains the libraries directly, just like a bundle that webpack generates.
In the output, the forbidden modules (i.e. `path`) are deferred to be required so that they are not loaded in a v8 context (See [electron-link's document for more detail](https://github.com/atom/electron-link).

### Create the v8 snapshots with `mksnapshot`

Now we've got a snapshotable script to generate the V8 snapshots.
Run the below script to do so:

```js
const outputBlobPath = baseDirPath
console.log(`Generating startup blob in "${outputBlobPath}"`)
childProcess.execFileSync(
  path.resolve(
    __dirname,
    '..',
    'node_modules',
    '.bin',
    'mksnapshot' + (process.platform === 'win32' ? '.cmd' : '')
  ),
  [snapshotScriptPath, '--output_dir', outputBlobPath]
)
```

Check out the entire script [here in the example repository](https://github.com/inkdropapp/electron-v8snapshots-example/blob/master/tools/create-v8-snapshots.js).

Finally, you will get `v8_context_snapshot.bin` file in your project directory.

### Load the snapshots in Electron

Let's load your V8 snapshots in your Electron app.
Electron has a default V8 snapshot file in its binary.
You have to overwrite it with yours.
Here is the path to the V8 snapshots in Electron:

 * macOS: `node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/`
 * Windows/Linux: `node_modules/electron/dist/`

You can copy your `v8_context_snapshot.bin` to there.
[Here](https://github.com/inkdropapp/electron-v8snapshots-example/blob/master/tools/copy-snapshot-bin-to-electron.js) is the script to copy the file.
Then, start your app and you should get `snapshotResult` variable in global context.
Type `snapshotResult` in the console to check if it exists.

Now, you've got the custom snapshots loaded in your Electron app.
How to load dependency libraries from them?

You have to override the default `require` function as following:

```js
const path = require('path')

console.log('snapshotResult:', snapshotResult)
if (typeof snapshotResult !== 'undefined') {
  console.log('snapshotResult available!', snapshotResult)

  const Module = require('module')
  const entryPointDirPath = path.resolve(
    global.require.resolve('react'),
    '..',
    '..',
    '..'
  )
  console.log('entryPointDirPath:', entryPointDirPath)

  Module.prototype.require = function (module) {
    const absoluteFilePath = Module._resolveFilename(module, this, false)
    let relativeFilePath = path.relative(entryPointDirPath, absoluteFilePath)
    if (!relativeFilePath.startsWith('./')) {
      relativeFilePath = `./${relativeFilePath}`
    }
    if (process.platform === 'win32') {
      relativeFilePath = relativeFilePath.replace(/\\/g, '/')
    }
    let cachedModule = snapshotResult.customRequire.cache[relativeFilePath]
    if (snapshotResult.customRequire.cache[relativeFilePath]) {
      console.log('Snapshot cache hit:', relativeFilePath)
    }
    if (!cachedModule) {
      console.log('Uncached module:', module, relativeFilePath)
      cachedModule = { exports: Module._load(module, this, false) }
      snapshotResult.customRequire.cache[relativeFilePath] = cachedModule
    }
    return cachedModule.exports
  }

  snapshotResult.setGlobals(
    global,
    process,
    window,
    document,
    console,
    global.require
  )
}
```

Note that you must run it before loading the libraries.
You should see outputs like "Snapshot cache hit: react" in the developer console if it works properly.
In the example project, you should see the result something like:

![Example result](https://dev-to-uploads.s3.amazonaws.com/i/jk6dg7io4s2ij97qyma7.png)

Congrats! You've got your app's dependencies loaded from the V8 snapshots.

## Eagerly constructing your app instance

Not only loading the dependencies from the cache, you can also use snapshots to construct your app instance like [Atom does](https://blog.atom.io/2017/04/18/improving-startup-time.html#eagerly-constructing-the-atom-global).
Some of the app construction tasks would be static and can be snapshotted, even though other tasks like reading the user's configuration are dynamic.
By pre-executing those initialization tasks using the snapshots, the launch speed can be improved further.
But that depends on your codebase.
For example, you can pre-construct React components in the snapshots.


---

That’s it! Hope it is helpful for your app development. Thank you for reading this.

I'm preparing to roll out the new version of Inkdrop with this improvement.
Hope you love it!

![Inkdrop v5](https://dev-to-uploads.s3.amazonaws.com/i/lebsvcgd622wesyossry.png)

#### See also

* [How I Kept My Solo Project Going Over 3 Years](https://blog.inkdrop.info/grit-46c57af01d6d)
* [Get A Slow Tempo — Towards Becoming A Long-Running Product](https://blog.inkdrop.info/get-a-slow-tempo-towards-becoming-a-long-running-product-12da66239539)
* [How I’ve Attracted The First 500 Paid Users For My SaaS That Costs $5/mo](https://blog.inkdrop.info/how-ive-attracted-the-first-500-paid-users-for-my-saas-that-costs-5-mo-7a5b94b8e820)

#### Thank you for all your support!

* Inkdrop Website: [https://www.inkdrop.app/](https://www.inkdrop.app/)
* Send feedback: [https://forum.inkdrop.app/](https://forum.inkdrop.app/)
* Contact us: [contact@inkdrop.app](mailto:contact@inkdrop.app)
* Twitter: [https://twitter.com/inkdrop_app](https://twitter.com/inkdrop_app)
* Instagram: [https://www.instagram.com/craftzdog/](https://www.instagram.com/craftzdog/)

[![](https://cdn-images-1.medium.com/max/3000/1*r-daItizHpAU-fQZk3ISag.png)](https://www.youtube.com/c/devaslife)

[![](https://cdn-images-1.medium.com/max/2372/1*-J5tp1_lulY9QknoLEv6zA.png)](http://eepurl.com/dNgJo6)