diff --git a/blog/a-faster-electron.md b/blog/a-faster-electron.md new file mode 100644 index 000000000..383e93473 --- /dev/null +++ b/blog/a-faster-electron.md @@ -0,0 +1,177 @@ +--- +title: 'A Faster Electron' +date: 2026-05-20T00:00:00.000Z +authors: MarshallOfSound +slug: a-faster-electron +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. 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](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 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](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. 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`](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_. + +
+ +
+ +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](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. + +### 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](https://github.com/electron/electron) is always looking for contributors. 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/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..d4ab70175 --- /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 + + + main + main-process flags + + + utility + utility-process flags + + + worker + worker flags + + + + RUNTIME + + + + + + + + + + + + + sandboxed renderer + deserializes the sandbox cache + + + main process + deserializes the main 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..aacae4e18 --- /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 + + + main + main-process flags + + + utility + utility-process flags + + + worker + worker flags + + + + RUNTIME + + + + + + + + + + + + + sandboxed renderer + deserializes the sandbox cache + + + main process + deserializes the main 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..a7d5886b6 --- /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% + main 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..12944aaa1 --- /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% + main 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..2792be37a --- /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 + + + Main 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..04def1257 --- /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 + + + Main 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. + + + + + + + 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"