codebytere

#52923: perf: cut first-window, ipc, contextBridge, Buffer and asar overhead

Merged
Created: Aug 17, 2026, 3:28:07 PM
Merged: Aug 18, 2026, 3:31:27 PM
4 comments
Target: main

Description of Change

Eight independent commits, batched to be easier on CI. Together on a Linux test app (main process loading ~1000 modules from app.asar, one window with a sandboxed preload): launch to first contentful paint 633 -> 452 ms, MB-scale IPC a third cheaper in both directions, bridged calls 25-57% cheaper, Buffer.allocUnsafe 6-80x cheaper, large files from the asar to the renderer as fast as plain file://. Each commit stands alone; its message carries the mechanism and the full numbers. Linux x64 release build, medians of interleaved fresh-process runs, all p < 0.01.

commit what gets faster before -> after
preallocate the file descriptor table before threads start (linux) first new BrowserWindow() on Linux; launch to first contentful paint 100 -> 18 ms; 610 -> 525 ms
stop rebuilding the whole path per component in asar::GetAsarArchivePath every fs lookup on a path inside an .asar (asar.splitPath()); launch to ready for the test app 5.3 -> 1.3 us; 393 -> 347 ms
keep context bridge proxy function state in the function's data array + trim per-value work when the bridge copies objects and arrays calls through contextBridge-exposed APIs: empty call / callback round trip / 400-item list 0.30 -> 0.13 us / 2.7 -> 1.9 us / 757 -> 684 us
give V8's in-sandbox allocator a PartitionAlloc backend outside renderers Buffer.allocUnsafe 1 MB; fs.promises.readFile 1 MB in the main process 297 -> 36 us; 1.2 -> 0.9 ms
serialize ipc values straight into the buffer they are sent in 1 MB ipcRenderer.invoke round trip; main -> renderer 1 MB; 4 MB round trip 4.2 -> 2.8 ms; 2.7 -> 1.6 ms; 14.2 -> 9.2 ms
size protocol and asar response pipes to the body 16 MB file from an .asar to the renderer; 16 MB protocol.handle body with a Content-Length 35 -> 22 ms; 49 -> 40 ms
remember module stat results for paths inside an archive repeated internalModuleStat misses during require() from an archive (1040 modules) 250 -> 245 ms

Per commit, briefly:

  • fd table (Linux only). The browser crosses 64 open descriptors while setting up the first renderer, and Linux's expand_fdtable() waits a full RCU grace period before freeing the old table once the process has threads; that wait was 55-116 ms of uninterruptible sleep inside window creation on the test host (10-30 ms is typical on a workstation). content's zygote already grows the table for the children it forks; main() now does the same for the browser and exec'd children with one F_DUPFD_CLOEXEC while still single-threaded (1024 entries, 8 KB).

  • GetAsarArchivePath. Walks component boundaries as offsets instead of DirName()/MatchesExtension() per component plus GetComponents() twice; byte-identical outputs, three new asar-spec cases for the corner cases.

  • contextBridge. Proxy function state moves from three private-property lookups per call to a three-slot array in the function's data; per copied value, the inner trace event goes, the Blink wrapper probes only run for API wrappers, one scope pair per property, bulk v8::Array::New. What is copied or proxied and all error paths are unchanged.

  • in-sandbox allocator. Node allocates unpooled Buffers through V8's default in-sandbox allocator, which V8 documents as a slow placeholder (page-granular, global mutex, zero-fills everything); Blink replaces it for renderers, the browser/utility/run-as-node processes never did. JavascriptEnvironment now installs one backed by gin's array-buffer partition via v8::V8::SetInSandboxAllocator, with allocations PartitionAlloc refuses (~2 GiB and up) served from the sandbox address space so huge Buffer.allocUnsafe calls keep working. Give-back: page zeroing moves to first touch, so Buffer.concat of MB inputs measures a few percent slower, which is upstream Node's behavior.

  • ipc payloads. Electron's own IPC mojom switches from blink.mojom.CloneableMessage to electron.mojom.SerializedValue (BigBuffer + length): the V8 serializer writes large values straight into the shared memory region that is sent, and a renderer parses a received region in place; the browser still parses a private copy because the sender keeps write access to shared memory. New api-ipc spec round-trips payloads across the 64 KB threshold up to 3 MB. Small messages, MessagePort traffic and startup unchanged.

  • response pipes. Electron's URL loaders used mojo's 64 KB default pipe (the asar loader a hard-coded 65536); where the body length is known before the pipe is created it is sized to the body with upstream's caps (2 MB for archive entries like file://, 512 KB for string/buffer and Content-Length responses); unknown-length streams and small responses keep exactly the pipe they had.

  • module stat cache. Node's own statCache only lives for one depth-0 execution, so repeated internalModuleStat misses on archive paths across separate require chains re-probe the archive; a bounded map keyed by the exact path remembers answers derived from an already-open (immutable) header. Small on a single startup graph, useful for lazy-require patterns.

Testing: full main-process suite identical to the baseline before/after with all seven applied; ASAN pass over the C++ commits clean; the asar and ipc commits add specs.

Checklist

Release Notes

Notes:

  • Fixed a stall of tens of milliseconds while creating the first BrowserWindow on Linux.
  • Improved the performance of file lookups inside ASAR archives and of large files served from them to the renderer.
  • Improved the performance of calls through contextBridge-exposed APIs.
  • Improved the performance of Buffer.allocUnsafe() and of large file reads in the main and utility processes.
  • Reduced the main-thread cost of sending large values through ipcRenderer, ipcMain handlers and webContents.send.

Backports

42-x-y
Pending
Waiting for a manual backport
43-x-y
Pending
Waiting for a manual backport
44-x-y
Pending
Waiting for a manual backport

Semver Impact

Major
Breaking changes
Minor
New features
Patch
Bug fixes
None
Docs, tests, etc.

Semantic Versioning helps users understand the impact of updates:

  • Major (X.y.z): Breaking changes that may require code modifications
  • Minor (x.Y.z): New features that maintain backward compatibility
  • Patch (x.y.Z): Bug fixes that don't change the API
  • None: Changes that don't affect using facing parts of Electron