Optimize your Electron app startup time

December 21, 2018

Electron apps can have sub-par performances compared to their native counterparts.

By applying some optimisations to your app you can make the startup feel much faster.

On this app (a music player), here is what happens (in short) on startup:

  • HTML & CSS are loaded and rendered
  • Javascript is asynchronously loaded
  • Dependencies are loaded (the longest)
  • JS starts and sets the dynamic elements in place (track lists, buttons, etc…)

As an end-user, you used to first see the basic HTML structure then after a few seconds element starts to appear where they need to be and fill the UI.

Why not save a snapshot of the DOM so we can directly show it when we restart the app?

Turns out it can be done pretty trivially, altough there was a few headaches on the path.

The code

Everything is done from the main process.

First a function saves the current HTML state of the app to a file. It contains the current version of the app in its name.

If in the future you modify the HTML structure, your users will use the updated version instead of an old cached one.

const saveState = () => {
	console.log('Saving window for faster startup...')

	// regex .replace is for escaping mfucking windows paths
	let writePath = path.join(app.getPath('userData'), 'harmony'+app.getVersion()+'_index.html').replace(/\\/g, "\\\\") 

	// Cache rendered html for faster startup 🚀
	
	window.webContents.executeJavaScript(`
		
		// This part depends on your app
		// In this case, I reset some elements to their original state before saving the dom

		// Reset ui elements

		getById('playerBufferBar').style.transform = getById('playerProgressBar').style.transform = 'translateX(0%)'
		
		addClass('playpauseIcon', 'icon-play')
		removeClass('playpauseIcon', 'icon-pause')
		removeClass(".playingIcon", "blink")
		addClass('refreshStatus', 'hide')

		// Here we write the DOM to the 'userData' folder
		// so we can use this for the next startup

		fs.writeFileSync("${writePath}",  '<!DOCTYPE html>'+document.documentElement.outerHTML)

		// Save settings
		store.set('settings', settings)
	
	`)
}

Reset/remove the elements of the UI you want in place and save the HTML to a new file in the userData directory.

Make sure your saved HTML begins with <!DOCTYPE html> or it won’t work when we loading from the Data URL. Spent a while on that one :P

Now we invoke the function on before-quit, every time we close the app.

app.on('before-quit', () =>  {
	willQuitApp = true
	
	saveState()
})

Also call it from your app window’s close event:

window.on('close', (e) => {
	saveState()

	if (willQuitApp || process.platform !== 'darwin') {
		/* the user tried to quit the app */
		window = null
	} else {
		/* the user only tried to close the window */
		e.preventDefault()
		window.hide()
	}
})

Now, if the user has the cached index.html in his cache load it will use it instead of the default index.html .

let cachedIndex = path.join(app.getPath('userData'), `/your_app${app.getVersion()}_index.html`) // Used for faster startup

let indexToPull = fs.existsSync(cachedIndex) ? cachedIndex : path.join(__dirname, '/app/index.html')

// We read the file content 
// so the Node context isn't in the userData folder
const pageContent = fs.readFileSync(indexToPull,'utf8')

window.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(pageContent), {
	baseURLForDataURL: `file://${__dirname}/app/`
})

Depending on if a cached HTML exists, we load the corresponding file.

We can’t load the cached file from its path as we’ll be placed in the userData directory and won’t be able to access any static files (CSS, JS, images).

So we have to read it and then send it as a Data URL to be rendered. Now however, your JS context will be ‘placed’ in the same directory as your main process file.

Say you have in app.js some local dependencies like require('./utils/db') - well it won’t work. You’re relative to your main folder.

The caveat to this technique is even if the UI appears loaded, you can’t interact with the app until the proper JS is fully loaded.

This can be further optimized by loading first only the UI interaction code then the rest. For most users its not noticeable tho.

Maybe I’ll try to package that into a module if that can be useful to other apps.

Do you have a minute?

Nucleus provides analytics for your desktop apps so you can follow and understand your users.

Discover →