From 950f2e08ad5cf33c763da757add495988106744c Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 3 Jun 2026 16:38:53 -0700 Subject: [PATCH 1/4] blog: A Faster Electron Two-part performance deep dive: startup work (sync-IPC removal, build-time V8 code cache for the framework bundles, Node.js startup snapshot for the browser process) and the compiler-optimization work (ThinLTO --lto-O2 and Electron-trained PGO profiles replacing Chrome's). Includes ten theme-responsive SVG diagrams. --- blog/a-faster-electron.md | 189 ++++++++++++++++++ .../assets/img/blog/code-cache-flow-dark.svg | 84 ++++++++ .../assets/img/blog/code-cache-flow-light.svg | 84 ++++++++ static/assets/img/blog/cold-marking-dark.svg | 68 +++++++ static/assets/img/blog/cold-marking-light.svg | 68 +++++++ .../assets/img/blog/extend-not-build-dark.svg | 33 +++ .../img/blog/extend-not-build-light.svg | 33 +++ .../img/blog/faster-startup-hero-dark.svg | 82 ++++++++ .../img/blog/faster-startup-hero-light.svg | 82 ++++++++ static/assets/img/blog/frame-budget-dark.svg | 47 +++++ static/assets/img/blog/frame-budget-light.svg | 47 +++++ static/assets/img/blog/pgo-coverage-dark.svg | 51 +++++ static/assets/img/blog/pgo-coverage-light.svg | 51 +++++ .../assets/img/blog/snapshot-layers-dark.svg | 29 +++ .../assets/img/blog/snapshot-layers-light.svg | 29 +++ .../img/blog/speedometer-stack-dark.svg | 49 +++++ .../img/blog/speedometer-stack-light.svg | 49 +++++ .../assets/img/blog/startup-timeline-dark.svg | 51 +++++ .../img/blog/startup-timeline-light.svg | 51 +++++ static/assets/img/blog/sync-ipc-dark.svg | 66 ++++++ static/assets/img/blog/sync-ipc-light.svg | 66 ++++++ 21 files changed, 1309 insertions(+) create mode 100644 blog/a-faster-electron.md create mode 100644 static/assets/img/blog/code-cache-flow-dark.svg create mode 100644 static/assets/img/blog/code-cache-flow-light.svg create mode 100644 static/assets/img/blog/cold-marking-dark.svg create mode 100644 static/assets/img/blog/cold-marking-light.svg create mode 100644 static/assets/img/blog/extend-not-build-dark.svg create mode 100644 static/assets/img/blog/extend-not-build-light.svg create mode 100644 static/assets/img/blog/faster-startup-hero-dark.svg create mode 100644 static/assets/img/blog/faster-startup-hero-light.svg create mode 100644 static/assets/img/blog/frame-budget-dark.svg create mode 100644 static/assets/img/blog/frame-budget-light.svg create mode 100644 static/assets/img/blog/pgo-coverage-dark.svg create mode 100644 static/assets/img/blog/pgo-coverage-light.svg create mode 100644 static/assets/img/blog/snapshot-layers-dark.svg create mode 100644 static/assets/img/blog/snapshot-layers-light.svg create mode 100644 static/assets/img/blog/speedometer-stack-dark.svg create mode 100644 static/assets/img/blog/speedometer-stack-light.svg create mode 100644 static/assets/img/blog/startup-timeline-dark.svg create mode 100644 static/assets/img/blog/startup-timeline-light.svg create mode 100644 static/assets/img/blog/sync-ipc-dark.svg create mode 100644 static/assets/img/blog/sync-ipc-light.svg diff --git a/blog/a-faster-electron.md b/blog/a-faster-electron.md new file mode 100644 index 000000000..4673f34f3 --- /dev/null +++ b/blog/a-faster-electron.md @@ -0,0 +1,189 @@ +--- +title: 'A Faster Electron' +date: 2026-05-20T00:00:00.000Z +authors: MarshallOfSound +slug: a-faster-electron +tags: [internals] +--- + +import ThemedImage from '@theme/ThemedImage'; + +Over the last few releases we've been making Electron faster. Not one feature, and not one benchmark: startup, IPC, `contextBridge`, networking, module loading, and raw JavaScript throughput, across every app that runs on Electron. + +
+ +
+ +The short version: **sandboxed renderers** start up **~43%** faster, the **browser process** boots **~40%** faster, and Electron's compiled code itself got faster across the board: **Speedometer is up ~17%**, `contextBridge` calls are up **28-50%**, and networking is up **19-40%**. None of it requires a single change to your app. + +This post is in two parts. **Part one is startup**: three changes that shrink the time between launching an app and seeing pixels. **Part two is everything after startup**: the discovery that Electron has been shipping with _Chrome's_ compiler optimization data for years, and what fixing that is worth. + + + +## 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're _deserialized_ instead of _compiled_. +- **A Node.js startup snapshot for the browser process**, so the Node bootstrap is _restored_ instead of _executed_. + +
+ +
+ +### Getting the sandboxed renderer off synchronous IPC + +A sandboxed renderer historically bootstrapped by asking the browser process for its preload scripts and metadata over a **synchronous** IPC message, then blocking until the answer came back. The catch: at startup the browser 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 browser process already knows everything the renderer needs, so it now **pushes** that data down with the frame-creation parameters over Mojo. No round-trip, no blocking, 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 compiles 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. We now generate that cache _at build time_ and embed it next to the source, so no process ever compiles the framework bundles again. + +The catch is that a V8 code cache is keyed to the exact V8 configuration that produced it: the V8 version, the source, the isolate's snapshot checksum, and a hash of V8's non-default flags. If anything differs, V8 silently rejects the cache and compiles from source; no error, just no speedup. And those keys **differ per process type**: a sandboxed renderer, a normal renderer, and the browser process each run V8 with different flags. So 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's on every launch, embedded in the binary, with no warm-up. 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. Which brings us to the next change. + +### A Node.js startup snapshot for the browser 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, and neither is Node's: + +1. **V8 startup snapshot**: V8's read-only heap and a bare context. +2. **Blink context snapshot**: the DOM bindings, with zero compiled JavaScript. +3. **Node snapshot**: the bootstrapped Node environment. _This is the missing one._ + +
+ +
+ +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 **browser process only**: a renderer's isolate already comes from the Blink snapshot, and V8 allows one snapshot per process. The browser process has no Blink, so Node's snapshot becomes its process-wide blob, and `node::CreateEnvironment` deserializes the environment instead of bootstrapping it. + +One trap worth sharing if you benchmark this kind of work: measure from process **spawn**, in a **release** build. Local debug-ish builds make snapshot deserialization look artificially slow, and measuring from your entry script's first line misses the entire point, because the win is everything that happens _before_ your script runs. Measured correctly, the Node snapshot is worth roughly **40% of browser-process startup**, about 50 ms on the hardware tested. + +### Startup, summed up + +- **Sandboxed renderers** skip a synchronous IPC round-trip and reuse cached preload bytecode: **~43% faster**. +- **Every process** deserializes framework JavaScript instead of compiling it: **~35%** off the pre-paint window. +- **The browser process** restores its Node.js bootstrap from a snapshot: **~40% faster** to first user JavaScript. + +The hardest part of all three wasn't the optimization, it was the seams: 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 an uncomfortable discovery about how Electron has been built for years. + +## 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. Here's the uncomfortable part: **Electron's release builds have been applying Chrome's profile.** Not a profile of Electron. Chrome's. + +### 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. + +### Fix one: turn 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 on an M5 MacBook (and more on older hardware). Useful, but as it turns out, the smaller half of the story. + +### Fix two: 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: + +| 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 **+17% end to end**, and the biggest single step is Electron's own profile, not the compiler flag. The borrowed optimization data really was the main problem. + +
+ +
+ +We also validated the whole stack against the official nightly across a 38-test suite covering `contextBridge`, IPC, and networking, with statistical significance testing: + +**37 significant improvements, zero regressions, one tie.** + +| 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 browser 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. + +If this kind of work sounds fun, the [Electron repository](https://github.com/electron/electron) is always looking for contributors. diff --git a/static/assets/img/blog/code-cache-flow-dark.svg b/static/assets/img/blog/code-cache-flow-dark.svg new file mode 100644 index 000000000..5461ef703 --- /dev/null +++ b/static/assets/img/blog/code-cache-flow-dark.svg @@ -0,0 +1,84 @@ + + + + + + + + One code cache per process flavor + + + BUILD TIME + + + + + Electron framework JS + embedded in the binary as source + + + + compiled at build time + once per process flavor + + + + + + + + + + + + + + sandbox + no Node env flags + + + renderer + +Node env flags + + + browser + main-process flags + + + utility + utility-process flags + + + worker + worker flags + + + + RUNTIME + + + + + + + + + + + + + sandboxed renderer + deserializes the sandbox cache + + + browser process + deserializes the browser cache + + + utility process + deserializes the utility cache + + + Each process's V8 flags are part of the cache key. The right match deserializes instantly; + a mismatch is silently rejected and V8 falls back to compiling from source. That's why one cache can't serve every process. + diff --git a/static/assets/img/blog/code-cache-flow-light.svg b/static/assets/img/blog/code-cache-flow-light.svg new file mode 100644 index 000000000..f0cdb90ad --- /dev/null +++ b/static/assets/img/blog/code-cache-flow-light.svg @@ -0,0 +1,84 @@ + + + + + + + + One code cache per process flavor + + + BUILD TIME + + + + + Electron framework JS + embedded in the binary as source + + + + compiled at build time + once per process flavor + + + + + + + + + + + + + + sandbox + no Node env flags + + + renderer + +Node env flags + + + browser + main-process flags + + + utility + utility-process flags + + + worker + worker flags + + + + RUNTIME + + + + + + + + + + + + + sandboxed renderer + deserializes the sandbox cache + + + browser process + deserializes the browser cache + + + utility process + deserializes the utility cache + + + Each process's V8 flags are part of the cache key. The right match deserializes instantly; + a mismatch is silently rejected and V8 falls back to compiling from source. That's why one cache can't serve every process. + diff --git a/static/assets/img/blog/cold-marking-dark.svg b/static/assets/img/blog/cold-marking-dark.svg new file mode 100644 index 000000000..ce00bbb42 --- /dev/null +++ b/static/assets/img/blog/cold-marking-dark.svg @@ -0,0 +1,68 @@ + + + + + + + + PGO is symmetric: what you don't train gets pessimized + The same optimized build, measured with two different training profiles + + + + + TRAINED ON BROWSER BENCHMARKS ONLY + + + Training ran: + + Speedometer + + DOM, CSS + + + Browser-style code + + +13% ✓ + + Node.js Buffer operations + + + −63% ✗ + + + Never ran Node, so the profile marked + all of Node cold. Worse than no profile. + + + TRAINED ON HOW APPS USE ELECTRON + + Training ran: + + Speedometer + + Node.js + + contextBridge + + IPC + + TLS + + ASAR + modules + + + Browser-style code + + +13% kept ✓ + + Node.js Buffer operations + + recovered ✓ + + + Training breadth is the whole game: + profile what apps actually do, not benchmarks. + + Buffer regression measured on macOS arm64; Speedometer on Linux x64. Both profiles applied to identical source. + diff --git a/static/assets/img/blog/cold-marking-light.svg b/static/assets/img/blog/cold-marking-light.svg new file mode 100644 index 000000000..0d46e0371 --- /dev/null +++ b/static/assets/img/blog/cold-marking-light.svg @@ -0,0 +1,68 @@ + + + + + + + + PGO is symmetric: what you don't train gets pessimized + The same optimized build, measured with two different training profiles + + + + + TRAINED ON BROWSER BENCHMARKS ONLY + + + Training ran: + + Speedometer + + DOM, CSS + + + Browser-style code + + +13% ✓ + + Node.js Buffer operations + + + −63% ✗ + + + Never ran Node, so the profile marked + all of Node cold. Worse than no profile. + + + TRAINED ON HOW APPS USE ELECTRON + + Training ran: + + Speedometer + + Node.js + + contextBridge + + IPC + + TLS + + ASAR + modules + + + Browser-style code + + +13% kept ✓ + + Node.js Buffer operations + + recovered ✓ + + + Training breadth is the whole game: + profile what apps actually do, not benchmarks. + + Buffer regression measured on macOS arm64; Speedometer on Linux x64. Both profiles applied to identical source. + diff --git a/static/assets/img/blog/extend-not-build-dark.svg b/static/assets/img/blog/extend-not-build-dark.svg new file mode 100644 index 000000000..9741ba2ce --- /dev/null +++ b/static/assets/img/blog/extend-not-build-dark.svg @@ -0,0 +1,33 @@ + + + + + + + + You don't build a heap, you extend one + + + + + BUILD FROM SCRATCH + + SnapshotCreator builds the heap + builtins + + + needs the full setup-isolate delegate + + + linked only into V8's own mksnapshot — not for embedders + + + EXTEND AN EXISTING SNAPSHOT + + load snapshot_blob.bin as the base + + + SnapshotCreator(params.snapshot_blob) + + + uses the deserialize delegate Electron already links ✓ + diff --git a/static/assets/img/blog/extend-not-build-light.svg b/static/assets/img/blog/extend-not-build-light.svg new file mode 100644 index 000000000..d724f4c95 --- /dev/null +++ b/static/assets/img/blog/extend-not-build-light.svg @@ -0,0 +1,33 @@ + + + + + + + + You don't build a heap, you extend one + + + + + BUILD FROM SCRATCH + + SnapshotCreator builds the heap + builtins + + + needs the full setup-isolate delegate + + + linked only into V8's own mksnapshot — not for embedders + + + EXTEND AN EXISTING SNAPSHOT + + load snapshot_blob.bin as the base + + + SnapshotCreator(params.snapshot_blob) + + + uses the deserialize delegate Electron already links ✓ + diff --git a/static/assets/img/blog/faster-startup-hero-dark.svg b/static/assets/img/blog/faster-startup-hero-dark.svg new file mode 100644 index 000000000..b8d9f1db0 --- /dev/null +++ b/static/assets/img/blog/faster-startup-hero-dark.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + ELECTRON · FASTER, NO API CHANGES + + + STARTUP + + + + + + ~43% + sandboxed renderer startup + preload setup off synchronous IPC * + + ~230 ms + + + ~130 ms + + + ~40% + browser process startup + Node.js bootstrap from a snapshot * + + ~125 ms + + + ~75 ms + + + EVERYTHING AFTER STARTUP + + + + + + +17% + Speedometer 3.1 + link-time optimization + Electron PGO + + + 56.6 + + + 66.2 + + + +28% + contextBridge calls + optimized with Electron's own profile + + + baseline + + + +28% + + + * Sample app, two ~150 KB preloads, cold launch, Apple Silicon.   † Speedometer on MacBook (M5); contextBridge geomean across 13 tests (Linux x64). Numbers vary by hardware. + diff --git a/static/assets/img/blog/faster-startup-hero-light.svg b/static/assets/img/blog/faster-startup-hero-light.svg new file mode 100644 index 000000000..07cd2ed4e --- /dev/null +++ b/static/assets/img/blog/faster-startup-hero-light.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + ELECTRON · FASTER, NO API CHANGES + + + STARTUP + + + + + + ~43% + sandboxed renderer startup + preload setup off synchronous IPC * + + ~230 ms + + + ~130 ms + + + ~40% + browser process startup + Node.js bootstrap from a snapshot * + + ~125 ms + + + ~75 ms + + + EVERYTHING AFTER STARTUP + + + + + + +17% + Speedometer 3.1 + link-time optimization + Electron PGO + + + 56.6 + + + 66.2 + + + +28% + contextBridge calls + optimized with Electron's own profile + + + baseline + + + +28% + + + * Sample app, two ~150 KB preloads, cold launch, Apple Silicon.   † Speedometer on MacBook (M5); contextBridge geomean across 13 tests (Linux x64). Numbers vary by hardware. + diff --git a/static/assets/img/blog/frame-budget-dark.svg b/static/assets/img/blog/frame-budget-dark.svg new file mode 100644 index 000000000..d3cdadc28 --- /dev/null +++ b/static/assets/img/blog/frame-budget-dark.svg @@ -0,0 +1,47 @@ + + + + + + + + + + The same interaction, before and after + + + + + + 0 ms + 5 ms + 10 ms + 15 ms + 20 ms + + + + + 16.7 ms + 60 fps frame budget + + + before + + + + + + 19.7 ms + misses the frame: janky + + + after + + + 16.4 ms + inside the budget: smooth 60 fps + + + ~19.5% less CPU for the same work + diff --git a/static/assets/img/blog/frame-budget-light.svg b/static/assets/img/blog/frame-budget-light.svg new file mode 100644 index 000000000..0c38c10f8 --- /dev/null +++ b/static/assets/img/blog/frame-budget-light.svg @@ -0,0 +1,47 @@ + + + + + + + + + + The same interaction, before and after + + + + + + 0 ms + 5 ms + 10 ms + 15 ms + 20 ms + + + + + 16.7 ms + 60 fps frame budget + + + before + + + + + + 19.7 ms + misses the frame: janky + + + after + + + 16.4 ms + inside the budget: smooth 60 fps + + + ~19.5% less CPU for the same work + diff --git a/static/assets/img/blog/pgo-coverage-dark.svg b/static/assets/img/blog/pgo-coverage-dark.svg new file mode 100644 index 000000000..aa1233a97 --- /dev/null +++ b/static/assets/img/blog/pgo-coverage-dark.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + What Chrome's profile knows about Electron's code + Share of the code Electron actually executes, by how Chrome's published PGO profile treats it + + + + + + + + + + 74.5% optimized + name + hash match Chrome's + 19.3% + 6.2% + + + + + + Silently rejected + Same name, different code: + Skia SIMD raster kernels, + V8's JSON stringifier, Mojo, net + + + + + + + Not in Chrome's profile at all + All of Node.js, contextBridge, + Electron's own C++, libuv + + + + One quarter of the code Electron executes gets zero optimization guidance. + No error, no warning. The compiler just lays it out as cold. Among Electron's 2,000 hottest functions, that quarter carries 23% of all execution. + + Measured with llvm-profdata: Chrome's published Linux profile vs a profile of Electron running Electron workloads. + diff --git a/static/assets/img/blog/pgo-coverage-light.svg b/static/assets/img/blog/pgo-coverage-light.svg new file mode 100644 index 000000000..0f494fa41 --- /dev/null +++ b/static/assets/img/blog/pgo-coverage-light.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + What Chrome's profile knows about Electron's code + Share of the code Electron actually executes, by how Chrome's published PGO profile treats it + + + + + + + + + + 74.5% optimized + name + hash match Chrome's + 19.3% + 6.2% + + + + + + Silently rejected + Same name, different code: + Skia SIMD raster kernels, + V8's JSON stringifier, Mojo, net + + + + + + + Not in Chrome's profile at all + All of Node.js, contextBridge, + Electron's own C++, libuv + + + + One quarter of the code Electron executes gets zero optimization guidance. + No error, no warning. The compiler just lays it out as cold. Among Electron's 2,000 hottest functions, that quarter carries 23% of all execution. + + Measured with llvm-profdata: Chrome's published Linux profile vs a profile of Electron running Electron workloads. + diff --git a/static/assets/img/blog/snapshot-layers-dark.svg b/static/assets/img/blog/snapshot-layers-dark.svg new file mode 100644 index 000000000..be6b2f810 --- /dev/null +++ b/static/assets/img/blog/snapshot-layers-dark.svg @@ -0,0 +1,29 @@ + + + + + + + + Three snapshots, only one holds Node's bootstrap + + + + V8 startup snapshot snapshot_blob.bin + V8's read-only heap and a bare context. Pure V8 internals. + already loaded + + + + Blink context snapshot v8_context_snapshot.bin + DOM / Web-IDL binding templates. Zero compiled JavaScript. + already loaded + + + + Node snapshot node_snapshot.cc + The bootstrapped Node environment: process, primordials, the module loader. + the missing piece + + Layers 1 and 2 were always there, but neither captures Node's bootstrap. Layer 3 is the one that does. + diff --git a/static/assets/img/blog/snapshot-layers-light.svg b/static/assets/img/blog/snapshot-layers-light.svg new file mode 100644 index 000000000..bddec5313 --- /dev/null +++ b/static/assets/img/blog/snapshot-layers-light.svg @@ -0,0 +1,29 @@ + + + + + + + + Three snapshots, only one holds Node's bootstrap + + + + V8 startup snapshot snapshot_blob.bin + V8's read-only heap and a bare context. Pure V8 internals. + already loaded + + + + Blink context snapshot v8_context_snapshot.bin + DOM / Web-IDL binding templates. Zero compiled JavaScript. + already loaded + + + + Node snapshot node_snapshot.cc + The bootstrapped Node environment: process, primordials, the module loader. + the missing piece + + Layers 1 and 2 were always there, but neither captures Node's bootstrap. Layer 3 is the one that does. + diff --git a/static/assets/img/blog/speedometer-stack-dark.svg b/static/assets/img/blog/speedometer-stack-dark.svg new file mode 100644 index 000000000..04b147e1b --- /dev/null +++ b/static/assets/img/blog/speedometer-stack-dark.svg @@ -0,0 +1,49 @@ + + + + + + + + + Each layer stacks + Speedometer 3.1, MacBook (M5), identical source + + + + + + Stock Electron + + 56.6 + + + + ThinLTO --lto-O2 + + + + 59.2 (+5%) + + + + Electron C++ PGO + + + + + 65.5 (+11%, the big one) + + + + Electron V8 builtins PGO + + + + + + 66.2 + +17% total + + + stock + link-time optimization + Electron's own profiles (the borrowed-data fix) + diff --git a/static/assets/img/blog/speedometer-stack-light.svg b/static/assets/img/blog/speedometer-stack-light.svg new file mode 100644 index 000000000..d29f4b510 --- /dev/null +++ b/static/assets/img/blog/speedometer-stack-light.svg @@ -0,0 +1,49 @@ + + + + + + + + + Each layer stacks + Speedometer 3.1, MacBook (M5), identical source + + + + + + Stock Electron + + 56.6 + + + + ThinLTO --lto-O2 + + + + 59.2 (+5%) + + + + Electron C++ PGO + + + + + 65.5 (+11%, the big one) + + + + Electron V8 builtins PGO + + + + + + 66.2 + +17% total + + + stock + link-time optimization + Electron's own profiles (the borrowed-data fix) + diff --git a/static/assets/img/blog/startup-timeline-dark.svg b/static/assets/img/blog/startup-timeline-dark.svg new file mode 100644 index 000000000..7c32e0107 --- /dev/null +++ b/static/assets/img/blog/startup-timeline-dark.svg @@ -0,0 +1,51 @@ + + + + + + + + Where each win lands in startup + + + process + Chromium/V8 init + Node bootstrap + preload setup + framework JavaScript + paint + + + Browser process · spawn → first user JavaScript + + before + + + + + after + + + + Node bootstrap restored from snapshot + + + Sandboxed renderer · spawn → first paint + + before + + + + + + after + + + + + + preload setup pushed, not pulled + framework JS from code cache + + All three wins are off the critical path between launching your app and seeing its first pixels. + Bars are illustrative and not drawn to exact scale; see the figures above for measured numbers. + diff --git a/static/assets/img/blog/startup-timeline-light.svg b/static/assets/img/blog/startup-timeline-light.svg new file mode 100644 index 000000000..f39aa5ea6 --- /dev/null +++ b/static/assets/img/blog/startup-timeline-light.svg @@ -0,0 +1,51 @@ + + + + + + + + Where each win lands in startup + + + process + Chromium/V8 init + Node bootstrap + preload setup + framework JavaScript + paint + + + Browser process · spawn → first user JavaScript + + before + + + + + after + + + + Node bootstrap restored from snapshot + + + Sandboxed renderer · spawn → first paint + + before + + + + + + after + + + + + + preload setup pushed, not pulled + framework JS from code cache + + All three wins are off the critical path between launching your app and seeing its first pixels. + Bars are illustrative and not drawn to exact scale; see the figures above for measured numbers. + diff --git a/static/assets/img/blog/sync-ipc-dark.svg b/static/assets/img/blog/sync-ipc-dark.svg new file mode 100644 index 000000000..b888c7035 --- /dev/null +++ b/static/assets/img/blog/sync-ipc-dark.svg @@ -0,0 +1,66 @@ + + + + + + + + Why a 2 ms request takes 80 ms + + + BEFORE · SYNCHRONOUS PULL + + + main process + renderer + + + + + + + other startup work (keeps the thread busy) + + reply (~2 ms of actual work) + + + + blocked — main thread frozen, waiting + + resume at ~80 ms + + + + + sync request + + The reply is cheap, but it only runs once the main thread finishes everything ahead of it. + + + AFTER · ONE-WAY PUSH + + main process + renderer + + + + push bundle + sent during navigation setup, before the renderer needs it + + + + + resume + ~22 ms, flat regardless of main-process load + + + + + No round-trip, no waiting on a busy thread — the data is already there when the renderer starts. + + + + + + + diff --git a/static/assets/img/blog/sync-ipc-light.svg b/static/assets/img/blog/sync-ipc-light.svg new file mode 100644 index 000000000..c380ff1a2 --- /dev/null +++ b/static/assets/img/blog/sync-ipc-light.svg @@ -0,0 +1,66 @@ + + + + + + + + Why a 2 ms request takes 80 ms + + + BEFORE · SYNCHRONOUS PULL + + + main process + renderer + + + + + + + other startup work (keeps the thread busy) + + reply (~2 ms of actual work) + + + + blocked — main thread frozen, waiting + + resume at ~80 ms + + + + + sync request + + The reply is cheap, but it only runs once the main thread finishes everything ahead of it. + + + AFTER · ONE-WAY PUSH + + main process + renderer + + + + push bundle + sent during navigation setup, before the renderer needs it + + + + + resume + ~22 ms, flat regardless of main-process load + + + + + No round-trip, no waiting on a busy thread — the data is already there when the renderer starts. + + + + + + + From 6d484539da0ce3dd116d40a6afdab348a5387b5d Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 3 Jun 2026 16:38:54 -0700 Subject: [PATCH 2/4] feat: image zoom for blog images and a wider blog TOC - docusaurus-plugin-image-zoom: click-to-zoom lightbox for blog post images (linked images excluded so they navigate instead of zooming). - Widen the blog post table of contents from col--2 (~190px) to 20% so headings stop wrapping on every line; the post column gives up the difference. --- docusaurus.config.ts | 13 +++++++++++ package.json | 1 + src/css/custom.scss | 15 ++++++++++++ yarn.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 6e50d4d8a..e5f63a187 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -264,9 +264,22 @@ const config: Config = { indexName: 'electronjs', contextualSearch: true, }, + // Lightbox (click-to-zoom) for blog post images. + // Images wrapped in links are excluded so they navigate instead of zooming. + zoom: { + selector: '.blog-wrapper .markdown :not(a) > img', + background: { + light: 'rgba(255, 255, 255, 0.95)', + dark: 'rgba(27, 28, 38, 0.95)', + }, + config: { + margin: 24, + }, + }, } satisfies Preset.ThemeConfig, plugins: [ 'docusaurus-plugin-sass', + 'docusaurus-plugin-image-zoom', path.resolve(__dirname, './src/plugins/apps/index.ts'), path.resolve(__dirname, './src/plugins/releases/index.ts'), path.resolve(__dirname, './src/plugins/fiddle/index.ts'), diff --git a/package.json b/package.json index c93d2b114..ccb8630b6 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "adm-zip": "^0.5.14", "ajv": "^8.18.0", "clsx": "^1.1.1", + "docusaurus-plugin-image-zoom": "^3.0.1", "docusaurus-plugin-sass": "^0.2.1", "lucide-react": "^0.511.0", "prism-react-renderer": "^2.3.1", diff --git a/src/css/custom.scss b/src/css/custom.scss index 5a76edccc..4b1af093d 100644 --- a/src/css/custom.scss +++ b/src/css/custom.scss @@ -330,3 +330,18 @@ html { } } } + +// Give the blog post table of contents a bit more room. By default it's a +// `col--2` (~190px in the 1140px container), which wraps almost every +// heading; widen it to 20% and take the difference from the post column. +// Scoped by structure: only the blog layout has a `main.col--7` followed by +// a `.col--2` TOC in the same row (docs use different column classes). +.main-wrapper .row { + main.col.col--7 { + --ifm-col-width: 55%; + } + + main.col.col--7 ~ .col.col--2 { + --ifm-col-width: 20%; + } +} diff --git a/yarn.lock b/yarn.lock index 8d2afe735..552c5ca2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8198,6 +8198,18 @@ __metadata: languageName: node linkType: hard +"docusaurus-plugin-image-zoom@npm:^3.0.1": + version: 3.0.1 + resolution: "docusaurus-plugin-image-zoom@npm:3.0.1" + dependencies: + medium-zoom: "npm:^1.1.0" + validate-peer-dependencies: "npm:^2.2.0" + peerDependencies: + "@docusaurus/theme-classic": ">=3.0.0" + checksum: 10c0/c18599f9d29c849fb07baa038fffc0faf5287c3f46546f25af0c60313bfbc36dd9f6176dddd6abd9e332d6a812e8f666e064411395de25dc7ed0e577dbfc5962 + languageName: node + linkType: hard + "docusaurus-plugin-sass@npm:^0.2.1": version: 0.2.2 resolution: "docusaurus-plugin-sass@npm:0.2.2" @@ -8394,6 +8406,7 @@ __metadata: adm-zip: "npm:^0.5.14" ajv: "npm:^8.18.0" clsx: "npm:^1.1.1" + docusaurus-plugin-image-zoom: "npm:^3.0.1" docusaurus-plugin-sass: "npm:^0.2.1" dotenv: "npm:^16.0.3" execa: "npm:^5.0.0" @@ -12258,6 +12271,13 @@ __metadata: languageName: node linkType: hard +"medium-zoom@npm:^1.1.0": + version: 1.1.0 + resolution: "medium-zoom@npm:1.1.0" + checksum: 10c0/7d1f05e8eab045c33d7c04d4ee7bf04f5246cf7a720d7b5f5a51c36ab23666e363bcbb6bffae50b5948d5eb19361914cb0e26a1fce5c1fff7a266bc0217893f3 + languageName: node + linkType: hard + "memfs@npm:^4.43.1": version: 4.49.0 resolution: "memfs@npm:4.49.0" @@ -13882,6 +13902,22 @@ __metadata: languageName: node linkType: hard +"path-root-regex@npm:^0.1.0": + version: 0.1.2 + resolution: "path-root-regex@npm:0.1.2" + checksum: 10c0/27651a234f280c70d982dd25c35550f74a4284cde6b97237aab618cb4b5745682d18cdde1160617bb4a4b6b8aec4fbc911c4a2ad80d01fa4c7ee74dae7af2337 + languageName: node + linkType: hard + +"path-root@npm:^0.1.1": + version: 0.1.1 + resolution: "path-root@npm:0.1.1" + dependencies: + path-root-regex: "npm:^0.1.0" + checksum: 10c0/aed5cd290df84c46c7730f6a363e95e47a23929b51ab068a3818d69900da3e89dc154cdfd0c45c57b2e02f40c094351bc862db70c2cb00b7e6bd47039a227813 + languageName: node + linkType: hard + "path-scurry@npm:^1.11.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" @@ -15603,6 +15639,15 @@ __metadata: languageName: node linkType: hard +"resolve-package-path@npm:^4.0.3": + version: 4.0.3 + resolution: "resolve-package-path@npm:4.0.3" + dependencies: + path-root: "npm:^0.1.1" + checksum: 10c0/d2e7883a075b21fbf084f7615f9201e4d5aea6c22ba670dc66503a256c5eba5983d0822b9d51ef33303bfe9b0025916f622f6d780c42d7c020d826f8a9bc58fa + languageName: node + linkType: hard + "resolve-pathname@npm:^3.0.0": version: 3.0.0 resolution: "resolve-pathname@npm:3.0.0" @@ -17518,6 +17563,16 @@ __metadata: languageName: node linkType: hard +"validate-peer-dependencies@npm:^2.2.0": + version: 2.2.0 + resolution: "validate-peer-dependencies@npm:2.2.0" + dependencies: + resolve-package-path: "npm:^4.0.3" + semver: "npm:^7.3.8" + checksum: 10c0/0728592b335dbd5d1444019a4e36e34b4de3a8ade5a4970a5f87b40f881dedeff1a00eb3d79add155a4ab3b97c9990d11ed21d8a3d7dccadd129a5bdf5b02a5a + languageName: node + linkType: hard + "value-equal@npm:^1.0.1": version: 1.0.1 resolution: "value-equal@npm:1.0.1" From 19e62792e2642593b4ec2bcabe6cedcb3a6e3d98 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Fri, 5 Jun 2026 14:37:29 -0700 Subject: [PATCH 3/4] blog: editorial pass on A Faster Electron --- blog/a-faster-electron.md | 100 ++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 54 deletions(-) diff --git a/blog/a-faster-electron.md b/blog/a-faster-electron.md index 4673f34f3..d502a8282 100644 --- a/blog/a-faster-electron.md +++ b/blog/a-faster-electron.md @@ -8,15 +8,15 @@ tags: [internals] import ThemedImage from '@theme/ThemedImage'; -Over the last few releases we've been making Electron faster. Not one feature, and not one benchmark: startup, IPC, `contextBridge`, networking, module loading, and raw JavaScript throughput, across every app that runs on 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.
-The short version: **sandboxed renderers** start up **~43%** faster, the **browser process** boots **~40%** faster, and Electron's compiled code itself got faster across the board: **Speedometer is up ~17%**, `contextBridge` calls are up **28-50%**, and networking is up **19-40%**. None of it requires a single change to your app. +The short version: sandboxed renderers start up ~43% faster, the browser 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. **Part one is startup**: three changes that shrink the time between launching an app and seeing pixels. **Part two is everything after startup**: the discovery that Electron has been shipping with _Chrome's_ compiler optimization data for years, and what fixing that is worth. +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 has been shipping with _Chrome's_ compiler optimization data for years. @@ -26,9 +26,9 @@ Before any of your code runs, a freshly spawned Electron process loads the binar 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're _deserialized_ instead of _compiled_. -- **A Node.js startup snapshot for the browser process**, so the Node bootstrap is _restored_ instead of _executed_. +- 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 browser process, so the Node bootstrap gets restored instead of executed.
@@ -36,46 +36,46 @@ Part one removes that work from the critical path with three independent changes ### Getting the sandboxed renderer off synchronous IPC -A sandboxed renderer historically bootstrapped by asking the browser process for its preload scripts and metadata over a **synchronous** IPC message, then blocking until the answer came back. The catch: at startup the browser 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. +A sandboxed renderer historically bootstrapped by asking the browser process for its preload scripts and metadata over a synchronous IPC message, then blocking until the answer came back. The catch: at startup the browser 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 browser process already knows everything the renderer needs, so it now **pushes** that data down with the frame-creation parameters over Mojo. No round-trip, no blocking, and the synchronous message is gone entirely. The preload scripts also gained a **bytecode cache**, so repeat launches deserialize them instead of re-compiling. +The fix was to stop asking. The browser 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. +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 compiles 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. We now generate that cache _at build time_ and embed it next to the source, so no process ever compiles the framework bundles again. +Electron's framework JavaScript is embedded in the binary as source, and V8 compiles 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. We now generate that cache at build time and embed it next to the source, so no process ever compiles the framework bundles again. -The catch is that a V8 code cache is keyed to the exact V8 configuration that produced it: the V8 version, the source, the isolate's snapshot checksum, and a hash of V8's non-default flags. If anything differs, V8 silently rejects the cache and compiles from source; no error, just no speedup. And those keys **differ per process type**: a sandboxed renderer, a normal renderer, and the browser process each run V8 with different flags. So the build generates one cache per process flavor, and each process picks up the one that matches it. +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 browser 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 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%)** | +| Eager build-time cache | ~6.4 ms (-35%) | -That's on every launch, embedded in the binary, with no warm-up. 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. Which brings us to the next change. +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 browser 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. +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, and neither is Node's: -1. **V8 startup snapshot**: V8's read-only heap and a bare context. -2. **Blink context snapshot**: the DOM bindings, with zero compiled JavaScript. -3. **Node snapshot**: the bootstrapped Node environment. _This is the missing one._ +1. The V8 startup snapshot: V8's read-only heap and a bare context. +2. The Blink context snapshot: the DOM bindings, with zero compiled JavaScript. +3. The Node snapshot: the bootstrapped Node environment. This is the missing one.
@@ -83,89 +83,81 @@ Why? Electron already boots from two snapshots, and neither is Node's: 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 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 **browser process only**: a renderer's isolate already comes from the Blink snapshot, and V8 allows one snapshot per process. The browser process has no Blink, so Node's snapshot becomes its process-wide blob, and `node::CreateEnvironment` deserializes the environment instead of bootstrapping it. +The snapshot is consumed by the browser process only: a renderer's isolate already comes from the Blink snapshot, and V8 allows one snapshot per process. The browser process has no Blink, so Node's snapshot becomes its process-wide blob, and `node::CreateEnvironment` deserializes the environment instead of bootstrapping it. -One trap worth sharing if you benchmark this kind of work: measure from process **spawn**, in a **release** build. Local debug-ish builds make snapshot deserialization look artificially slow, and measuring from your entry script's first line misses the entire point, because the win is everything that happens _before_ your script runs. Measured correctly, the Node snapshot is worth roughly **40% of browser-process startup**, about 50 ms on the hardware tested. +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 browser-process startup, about 50 ms on the hardware tested. -### Startup, summed up +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. -- **Sandboxed renderers** skip a synchronous IPC round-trip and reuse cached preload bytecode: **~43% faster**. -- **Every process** deserializes framework JavaScript instead of compiling it: **~35%** off the pre-paint window. -- **The browser process** restores its Node.js bootstrap from a snapshot: **~40% faster** to first user JavaScript. - -The hardest part of all three wasn't the optimization, it was the seams: 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 an uncomfortable discovery about how Electron has been built for years. +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. +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. Here's the uncomfortable part: **Electron's release builds have been applying Chrome's profile.** Not a profile of Electron. Chrome's. +Chrome uses PGO aggressively, and Google publishes fresh Chrome profiles every few hours. Here's the uncomfortable part: Electron's release builds have been applying Chrome's profile. Not a profile of Electron. Chrome's. ### 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. +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_. +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. +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. ### Fix one: turn 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. +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 on an M5 MacBook (and more on older hardware). Useful, but as it turns out, the smaller half of the story. +Opting in is worth about +5% on Speedometer 3.1 on an M5 MacBook (and more on older hardware). Useful, but as it turns out, the smaller of the two fixes. ### Fix two: 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 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 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: -| 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% | +| 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 **+17% end to end**, and the biggest single step is Electron's own profile, not the compiler flag. The borrowed optimization data really was the main problem. +That's +17% end to end, and the biggest single step is Electron's own profile, not the compiler flag. The borrowed optimization data really was the main problem.
-We also validated the whole stack against the official nightly across a 38-test suite covering `contextBridge`, IPC, and networking, with statistical significance testing: - -**37 significant improvements, zero regressions, one tie.** +We also validated the whole stack against the official nightly across a 38-test suite covering `contextBridge`, IPC, and networking, with statistical significance testing: 37 significant improvements, zero regressions, and one tie. | Area | Improvement (geomean) | | ---------------------------------------- | --------------------- | -| `contextBridge` | **+28%** | -| Networking (`fetch`, WebSocket, `https`) | **+19%** | -| IPC | **+11%** | -| Overall | **+19.5%** | +| `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. @@ -181,8 +173,8 @@ A useful frame: a UI interaction that costs 19.7 ms today misses the 60 fps fram ## Putting it together -- **Startup**: sandboxed renderers start ~43% faster, framework JavaScript comes from an embedded code cache, and the browser 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. +- Startup: sandboxed renderers start ~43% faster, framework JavaScript comes from an embedded code cache, and the browser 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. From 5d8cc36f4aa42ec28e7cc23326544a17ca57e87b Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 7 Jun 2026 08:39:14 -0700 Subject: [PATCH 4/4] blog: address review feedback on A Faster Electron --- blog/a-faster-electron.md | 60 +++++++++---------- .../assets/img/blog/code-cache-flow-dark.svg | 8 +-- .../assets/img/blog/code-cache-flow-light.svg | 8 +-- .../img/blog/faster-startup-hero-dark.svg | 6 +- .../img/blog/faster-startup-hero-light.svg | 6 +- .../assets/img/blog/startup-timeline-dark.svg | 6 +- .../img/blog/startup-timeline-light.svg | 6 +- 7 files changed, 48 insertions(+), 52 deletions(-) diff --git a/blog/a-faster-electron.md b/blog/a-faster-electron.md index d502a8282..383e93473 100644 --- a/blog/a-faster-electron.md +++ b/blog/a-faster-electron.md @@ -3,20 +3,20 @@ title: 'A Faster Electron' date: 2026-05-20T00:00:00.000Z authors: MarshallOfSound slug: a-faster-electron -tags: [internals] +tags: [techtalk, internals] --- import ThemedImage from '@theme/ThemedImage'; -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. +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 browser 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. +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 has been shipping with _Chrome's_ compiler optimization data for years. +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. @@ -26,34 +26,34 @@ Before any of your code runs, a freshly spawned Electron process loads the binar 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. +- Pushing sandboxed-renderer startup data over [Mojo](https://chromium.googlesource.com/chromium/src/+/HEAD/mojo/README.md) 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 browser process, so the Node bootstrap gets restored instead of executed. +- 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 browser process for its preload scripts and metadata over a synchronous IPC message, then blocking until the answer came back. The catch: at startup the browser 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. +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 browser 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. +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 compiles 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. We now generate that cache at build time and embed it next to the source, so no process ever compiles the framework bundles again. +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 browser 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. +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. @@ -67,15 +67,11 @@ The clearest win is the sandboxed renderer, whose pre-paint blocking window is a 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 browser process +### 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, and neither is Node's: - -1. The V8 startup snapshot: V8's read-only heap and a bare context. -2. The Blink context snapshot: the DOM bindings, with zero compiled JavaScript. -3. The Node snapshot: the bootstrapped Node environment. This is the missing one. +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.
@@ -89,9 +85,9 @@ The way out is that you don't have to build a heap from scratch. You can extend
-The snapshot is consumed by the browser process only: a renderer's isolate already comes from the Blink snapshot, and V8 allows one snapshot per process. The browser process has no Blink, so Node's snapshot becomes its process-wide blob, and `node::CreateEnvironment` deserializes the environment instead of bootstrapping it. +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 browser-process startup, about 50 ms on the hardware tested. +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. @@ -99,15 +95,15 @@ Startup is half the story. The other half starts with something we found while l ## 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. +Modern compilers optimize code around how it actually runs. The biggest lever is [Profile-Guided Optimization](https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/pgo.md) (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. Here's the uncomfortable part: Electron's release builds have been applying Chrome's profile. Not a profile of Electron. Chrome's. +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_. +We measured it with [`llvm-profdata`](https://llvm.org/docs/CommandGuide/llvm-profdata.html): about a quarter of the code Electron executes gets zero optimization guidance, and it's concentrated in exactly the code that makes Electron _Electron_.
@@ -115,13 +111,13 @@ We measured it with `llvm-profdata`: about a quarter of the code Electron execut 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. -### Fix one: turn on link-time optimization +### 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 on an M5 MacBook (and more on older hardware). Useful, but as it turns out, the smaller of the two fixes. +Opting in is worth about +5% on [Speedometer 3.1](https://browserbench.org/Speedometer3.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. -### Fix two: Electron's own profiles +### 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. @@ -135,7 +131,7 @@ The training suite now covers what Electron apps actually do: main-process Node. ### The results -Each layer stacks on the last. On Speedometer 3.1, on an M5 MacBook: +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 | | -------------------------- | ----- | ---- | @@ -144,13 +140,13 @@ Each layer stacks on the last. On Speedometer 3.1, on an M5 MacBook: | + Electron C++ PGO | 65.5 | +11% | | + Electron V8 builtins PGO | 66.2 | +1% | -That's +17% end to end, and the biggest single step is Electron's own profile, not the compiler flag. The borrowed optimization data really was the main problem. +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.
-We also validated the whole stack against the official nightly across a 38-test suite covering `contextBridge`, IPC, and networking, with statistical significance testing: 37 significant improvements, zero regressions, and one tie. +The same builds, measured on Electron-specific workloads: | Area | Improvement (geomean) | | ---------------------------------------- | --------------------- | @@ -173,9 +169,9 @@ A useful frame: a UI interaction that costs 19.7 ms today misses the 60 fps fram ## Putting it together -- Startup: sandboxed renderers start ~43% faster, framework JavaScript comes from an embedded code cache, and the browser process restores its Node.js bootstrap from a snapshot. +- 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. +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](https://github.com/electron/electron) is always looking for contributors. diff --git a/static/assets/img/blog/code-cache-flow-dark.svg b/static/assets/img/blog/code-cache-flow-dark.svg index 5461ef703..d4ab70175 100644 --- a/static/assets/img/blog/code-cache-flow-dark.svg +++ b/static/assets/img/blog/code-cache-flow-dark.svg @@ -1,4 +1,4 @@ - + @@ -41,7 +41,7 @@ +Node env flags - browser + main main-process flags @@ -71,8 +71,8 @@ deserializes the sandbox cache - browser process - deserializes the browser cache + main process + deserializes the main cache utility process diff --git a/static/assets/img/blog/code-cache-flow-light.svg b/static/assets/img/blog/code-cache-flow-light.svg index f0cdb90ad..aacae4e18 100644 --- a/static/assets/img/blog/code-cache-flow-light.svg +++ b/static/assets/img/blog/code-cache-flow-light.svg @@ -1,4 +1,4 @@ - + @@ -41,7 +41,7 @@ +Node env flags - browser + main main-process flags @@ -71,8 +71,8 @@ deserializes the sandbox cache - browser process - deserializes the browser cache + main process + deserializes the main cache utility process diff --git a/static/assets/img/blog/faster-startup-hero-dark.svg b/static/assets/img/blog/faster-startup-hero-dark.svg index b8d9f1db0..a7d5886b6 100644 --- a/static/assets/img/blog/faster-startup-hero-dark.svg +++ b/static/assets/img/blog/faster-startup-hero-dark.svg @@ -1,4 +1,4 @@ - + @@ -39,9 +39,9 @@ ~130 ms - + ~40% - browser process startup + main process startup Node.js bootstrap from a snapshot * ~125 ms diff --git a/static/assets/img/blog/faster-startup-hero-light.svg b/static/assets/img/blog/faster-startup-hero-light.svg index 07cd2ed4e..12944aaa1 100644 --- a/static/assets/img/blog/faster-startup-hero-light.svg +++ b/static/assets/img/blog/faster-startup-hero-light.svg @@ -1,4 +1,4 @@ - + @@ -39,9 +39,9 @@ ~130 ms - + ~40% - browser process startup + main process startup Node.js bootstrap from a snapshot * ~125 ms diff --git a/static/assets/img/blog/startup-timeline-dark.svg b/static/assets/img/blog/startup-timeline-dark.svg index 7c32e0107..2792be37a 100644 --- a/static/assets/img/blog/startup-timeline-dark.svg +++ b/static/assets/img/blog/startup-timeline-dark.svg @@ -1,4 +1,4 @@ - + @@ -14,8 +14,8 @@ framework JavaScript paint - - Browser process · spawn → first user JavaScript + + Main process · spawn → first user JavaScript before diff --git a/static/assets/img/blog/startup-timeline-light.svg b/static/assets/img/blog/startup-timeline-light.svg index f39aa5ea6..04def1257 100644 --- a/static/assets/img/blog/startup-timeline-light.svg +++ b/static/assets/img/blog/startup-timeline-light.svg @@ -1,4 +1,4 @@ - + @@ -14,8 +14,8 @@ framework JavaScript paint - - Browser process · spawn → first user JavaScript + + Main process · spawn → first user JavaScript before