codebytere

#53115: perf: stop routing renderer requests and file loads through the main thread

Merged
Created: Aug 22, 2026, 9:49:39 AM
Merged: Aug 24, 2026, 7:02:55 AM
8 comments
Target: main

Description of Change

A renderer's network requests and file:// loads no longer wait for the main process's JavaScript. Today every URLLoaderFactory Electron hands a renderer is fronted by objects bound on the browser UI thread, so each <script>, stylesheet, fetch() or image request is dispatched by the main thread and sits in its queue while app code runs. Measured on Linux x64 release builds, medians of 3 warm loads, with the main process either idle or running 100 ms of JavaScript right after the navigation commits (which is what app startup looks like from the window's side):

idle main process main process busy 100 ms
page with 150 small subresources from a local server, no webRequest listeners 51 -> 46 ms 152 -> 51 ms
same page, session has a protocol.handle() for another scheme 48 -> 49 ms 152 -> 50 ms
same page, any webRequest listener registered (matching or not) 54 -> 54 ms 152 -> 155 ms (unchanged: still via the main thread)
page with 60 small stylesheets from disk (loadFile) 30 -> 28 ms 130 -> 30 ms (200 ms busy: 230 -> 31)
renderer fetch() round trip, no listeners / with a webRequest listener 0.43 -> 0.39 ms / 0.66 -> 0.65 ms
main-process CPU per renderer request, no listeners / with a listener 145 -> 130 us / 330 -> 335 us
main-process CPU per renderer console.log, no console-message listener 22.6 -> 13.4 us

Four commits, each independent:

  1. Keep renderer network requests off the main thread unless they are observed. WillCreateURLLoaderFactory wrapped every factory in a ProxyingURLLoaderFactory on the UI thread, listeners or not. Renderer-facing factories now get a small URLLoaderFactoryGate on the IO thread in front of the proxy: it forwards straight to the network service unless the session has a webRequest listener, an intercepted scheme matching the request, or an --ignore-connections-limit domain, in which case the request goes to the proxy exactly as before. That state is a few words under a lock that api::WebRequest and ProtocolRegistry update, so a listener added at any time applies to the next request without recreating factories (listeners re-run on redirects, which is why their URL filters are not applied at the gate). With a listener registered nothing changes, within noise.
  2. Bind the file:// URL loader factory off the main thread. AsarURLLoaderFactory only posts loaders to the thread pool, so it is now bound on the IO thread; file:// subresources stop queueing behind main-process work.
  3. Skip the trusted-header round trips for requests proxied without webRequest listeners. The proxy put every request in "extraHeaders" mode (two TrustedHeaderClient round trips per request); that is what lets listeners see and modify network-owned headers, so it stays whenever the session has any webRequest listener, and requests that reach the proxy only for an intercepted protocol or an --ignore-connections-limit domain skip it.
  4. Don't build console-message events while nothing listens for them. OnDidAddMessageToConsole created an event object and emitted into JS for every renderer console call; WebContents now keeps a flag from newListener/removeListener and returns early without it.

Checklist

Release Notes

Notes: Page resources, fetch() calls and file:// loads in renderers no longer wait for the main process to be idle when no webRequest listeners or protocol interceptors are registered.

Backports

42-x-y
Merged
PR Number
#53143
Merged At
Aug 24, 2026, 4:03:40 PM
Released In
Not yet
Release Date
Not yet
43-x-y
Merged
PR Number
#53142
Merged At
Aug 24, 2026, 4:45:19 PM
Released In
Not yet
Release Date
Not yet
44-x-y
In-flight
PR Number
#53141
Waiting to be merged

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