A Faster Electron
We've spent the last few releases making Electron faster. The work covers startup, IPC, contextBridge, networking, module loading, and raw JavaScript throughput, and it applies to every app that runs on Electron. It ships today in Electron 42.3.3, 43.0.0-beta.1, and 44.0.0-nightly.20260603.
The short version: sandboxed renderers start up ~43% faster, the main process boots ~40% faster, and Electron's compiled code got quicker across the board. Speedometer is up ~17%, contextBridge calls are up 28-50%, and networking is up 19-40%. You don't have to change a line of your app to get any of this.
This post is in two parts. The first is startup: three changes that shrink the time between launching an app and seeing pixels. The second is everything after startup, and it begins with the discovery that Electron's release builds have spent years borrowing Chrome's compiler optimization data, which is almost, but not quite, right for Electron.
Part one: faster startup
Before any of your code runs, a freshly spawned Electron process loads the binary, initializes Chromium and V8, bootstraps a Node.js environment (where it has one), and parses and compiles Electron's own framework JavaScript. The last two are pure CPU spent turning JavaScript into bytecode, and they happen on every launch of every process.
Part one removes that work from the critical path with three independent changes:
- Pushing sandboxed-renderer startup data over Mojo instead of a synchronous IPC, and caching the preload bytecode.
- A build-time V8 code cache for Electron's framework bundles, so they get deserialized instead of compiled.
- A Node.js startup snapshot for the main process, so the Node bootstrap gets restored instead of executed.
Getting the sandboxed renderer off synchronous IPC
A sandboxed renderer historically bootstrapped by asking the main process for its preload scripts and metadata over a synchronous IPC message, then blocking until the answer came back. The catch: at startup the main process is the busiest it will ever be, so the renderer's cheap request keeps getting preempted by everything else. A reply that takes 2 ms of actual work can land 80 ms later, and the renderer is frozen the whole time.
The fix was to stop asking. The main process already knows everything the renderer needs, so it now pushes that data down with the frame-creation parameters over Mojo, and the synchronous message is gone entirely. The preload scripts also gained a bytecode cache, so repeat launches deserialize them instead of re-compiling.
Together these make sandboxed renderer startup roughly 43% faster under real-world conditions, and the renderer's pre-paint time no longer depends on how busy your main process is.
A build-time code cache for the framework bundles
Electron's framework JavaScript is embedded in the binary as source, and V8 has always compiled it from scratch in every process, on every launch. V8 has a standard fix for this, a code cache: compile once, serialize the bytecode, deserialize on later runs. Electron just never used it. We now generate that cache at build time and embed it next to the source, so no process ever compiles the framework bundles again.
A code cache is only valid for the exact V8 configuration that produced it; if anything differs, V8 silently rejects it and compiles from source, and nothing tells you it happened. Since a sandboxed renderer, a normal renderer, and the main process each run V8 with different flags, the build generates one cache per process flavor, and each process picks up the one that matches it.
The cache is also built with eager compilation, so it covers every inner function rather than just the top level. The framework bundles run in full during bootstrap anyway; this just moves all of that compilation to build time.
The clearest win is the sandboxed renderer, whose pre-paint blocking window is almost entirely framework compilation:
| Pre-paint blocking window | |
|---|---|
| No cache (compile from source) | ~9.8 ms |
| Eager build-time cache | ~6.4 ms (-35%) |
That saving applies on every launch, without any warm-up, because the cache ships inside the binary. The Node-enabled processes consume the cache too, but their startup is dominated by something a code cache can't fix: the Node bootstrap itself.
A Node.js startup snapshot for the main process
A code cache skips compilation. The Node.js bootstrap is mostly execution: building process, wiring the module loader, running ~50 internal setup scripts. Node has a feature designed for exactly this, the startup snapshot: serialize a fully bootstrapped environment once, then deserialize it on every launch instead of re-running the bootstrap. Upstream Node ships with it on. Electron has had it disabled for years.
Why? Electron already boots from two snapshots, the V8 startup snapshot (V8's read-only heap and a bare context) and the Blink context snapshot (the DOM bindings, with zero compiled JavaScript), and neither captures Node's bootstrap. The Node snapshot would be the missing third layer.
Building the missing one looked impossible at first. Creating a snapshot appears to require a special from-scratch build of V8 that only V8's own tooling gets to use; embedders like Node and Electron only get the "deserialize from an existing snapshot" build, and trying to create a snapshot with it fails with Heap setup supported only in mksnapshot.
The way out is that you don't have to build a heap from scratch. You can extend an existing snapshot: load the V8 startup snapshot as a base, run Node's bootstrap on top of it, and serialize the result. That's exactly how Chromium builds the Blink context snapshot on every build, with the same "deserialize-only" V8 that Electron has.
The snapshot is consumed by the main process only: a renderer's isolate already comes from the Blink snapshot, and V8 allows one snapshot per process. The main process has no Blink, so Node's snapshot becomes its process-wide blob, and node::CreateEnvironment deserializes the environment instead of bootstrapping it.
Measured from process spawn in a release build (the win is everything that happens before your entry script runs), the Node snapshot is worth roughly 40% of main-process startup, about 50 ms on the hardware tested.
For all three changes, the hard part was the seams rather than the optimizations themselves: Chromium, V8, and Node each have their own model of how a process boots, and the bugs live where those models meet.
Startup is half the story. The other half starts with something we found while looking at how Electron's release builds are compiled.
Part two: Electron has been shipping with Chrome's optimization data
Modern compilers optimize code around how it actually runs. The biggest lever is Profile-Guided Optimization (PGO): run an instrumented build through real workloads, record which functions are hot, then rebuild with that profile so the compiler knows what to inline, how to lay out branches, and what to keep in the hot path.
Chrome uses PGO aggressively, and Google publishes fresh Chrome profiles every few hours. Electron's release builds have been applying Chrome's profile rather than one trained on Electron. That profile is mostly right for Electron too, which is exactly why the parts it gets wrong went unnoticed.
What borrowing a profile costs
A profile matches functions by name plus a hash of their code. Functions that exist in Electron but not Chrome (all of Node.js, all of Electron's own C++, contextBridge) were never in Chrome's profile. Functions that exist in both but are compiled differently in Electron (different patches, flags, V8 configuration) match by name but fail the hash check and are silently rejected. Either way, the compiler gets no guidance and lays the code out as cold.
We measured it with llvm-profdata: about a quarter of the code Electron executes gets zero optimization guidance, and it's concentrated in exactly the code that makes Electron Electron.
This isn't theoretical. While doing this work we found that crypto.randomBytes in Electron 44 runs at less than half its Electron 42 speed. Nobody touched the crypto code: a BoringSSL patch changed the functions' hashes, Chrome's profile silently stopped covering them, and the compiler started treating them as cold. That's what makes a borrowed profile insidious: code gets slower without anyone changing it, and nothing warns you. With an Electron profile, the regression disappears.
Turning on link-time optimization
Chromium links with ThinLTO, which lets the compiler optimize across source files at link time, but the default setting does no optimization at all (--lto-O0). Chrome's release builds opt into --lto-O2. Electron never did.
Opting in is worth about +5% on Speedometer 3.1, the industry-standard benchmark of web-app responsiveness, on an M5 MacBook (and more on older hardware). Useful, but as it turns out, the smaller of the two fixes.
Electron's own profiles
If borrowing Chrome's profile is the problem, the fix is to train our own: instrumented builds for every release platform, training workloads that exercise Electron the way apps actually use it, and a pipeline that publishes the profiles for release builds to consume.
The training workloads turn out to be the entire game, because PGO is symmetric: everything the training runs gets optimized, and everything it doesn't run gets explicitly laid out as cold. Our first profile was trained on browser benchmarks. Browser-style code got faster, and Node.js Buffer operations got 63% slower than stock, because the training never ran Node.
The training suite now covers what Electron apps actually do: main-process Node.js, contextBridge and IPC marshaling, networking over real TLS, module loading from ASAR archives, and compression. It also covers V8's builtins, which have their own separate profile; Chrome's version rejects every promise and async builtin in Electron (we build V8 with promise hooks enabled, Chrome doesn't), so those were running unoptimized too.
The results
Each layer stacks on the last. On Speedometer 3.1, on an M5 MacBook, starting from a stock nightly build from before this work landed:
| Configuration | Score | Step |
|---|---|---|
| Stock Electron | 56.6 | |
+ ThinLTO --lto-O2 | 59.2 | +5% |
| + Electron C++ PGO | 65.5 | +11% |
| + Electron V8 builtins PGO | 66.2 | +1% |
That's a +17% end-to-end score increase, with the dedicated Electron performance profiles providing most of the results. Other platforms show similar effects, though we haven't measured them as precisely.
The same builds, measured on Electron-specific workloads:
| Area | Improvement (geomean) |
|---|---|
contextBridge | +28% |
Networking (fetch, WebSocket, https) | +19% |
| IPC | +11% |
| Overall | +19.5% |
The wins land exactly where Electron's own code runs: contextBridge calls that round-trip objects are up 40-55%, fetch round-trips are up 23-40%, IPC payloads of every size are up 7-16%. On identical workloads, an optimized Electron now matches or exceeds Chrome itself; the penalty for being "Chrome plus Node.js" instead of Chrome is gone.
What this means for your app
The same app, on the same hardware, spends less CPU doing what it already does. Chat apps: channel switching and message rendering cost 16-20% less CPU, and every preload API call is 28-50% cheaper. Editors: module loading from ASAR is 8-10% faster and Buffer-heavy work is no longer pessimized. Document apps: JSON and structured clone of cached data are 13-37% faster.
A useful frame: a UI interaction that costs 19.7 ms today misses the 60 fps frame budget and feels janky. At ~19.5% less CPU it costs about 16.4 ms, inside the budget. These changes move real interactions across that line.
Putting it together
- Startup: sandboxed renderers start ~43% faster, framework JavaScript comes from an embedded code cache, and the main process restores its Node.js bootstrap from a snapshot.
- Everything after: Electron stopped borrowing Chrome's compiler optimization data and started generating its own, removing a silent ~20-25% CPU penalty on its hottest code paths.
Apps don't need to do anything except update: everything here ships in Electron 42.3.3, 43.0.0-beta.1, and 44.0.0-nightly.20260603.
If this kind of work sounds fun, the Electron repository is always looking for contributors.
