#53115: perf: stop routing renderer requests and file loads through the main thread
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:
- Keep renderer network requests off the main thread unless they are observed.
WillCreateURLLoaderFactorywrapped every factory in aProxyingURLLoaderFactoryon the UI thread, listeners or not. Renderer-facing factories now get a smallURLLoaderFactoryGateon the IO thread in front of the proxy: it forwards straight to the network service unless the session has awebRequestlistener, an intercepted scheme matching the request, or an--ignore-connections-limitdomain, in which case the request goes to the proxy exactly as before. That state is a few words under a lock thatapi::WebRequestandProtocolRegistryupdate, 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. - Bind the file:// URL loader factory off the main thread.
AsarURLLoaderFactoryonly posts loaders to the thread pool, so it is now bound on the IO thread; file:// subresources stop queueing behind main-process work. - Skip the trusted-header round trips for requests proxied without webRequest listeners. The proxy put every request in "extraHeaders" mode (two
TrustedHeaderClientround trips per request); that is what lets listeners see and modify network-owned headers, so it stays whenever the session has anywebRequestlistener, and requests that reach the proxy only for an intercepted protocol or an--ignore-connections-limitdomain skip it. - Don't build
console-messageevents while nothing listens for them.OnDidAddMessageToConsolecreated an event object and emitted into JS for every renderer console call;WebContentsnow keeps a flag fromnewListener/removeListenerand returns early without it.
Checklist
- PR description included and stakeholders cc'd
-
npm testpasses (webRequest, protocol, session, net, BrowserWindow, webContents, asar, webview, subframe, service worker, extensions and chromium specs locally on Linux) - PR release notes describe the change in a way relevant to app developers, and are capitalized, punctuated, and past tense.
- I have reviewed and verified the changes
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
Semver Impact
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