Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.4 KiB
Electron WebSocket Fix
Patched Electron builds that fix a Chromium regression where continuous mouse input starves WebSocket and Worker message dispatch when --disable-frame-rate-limit is active.
This is critical for competitive browser-based games (like Krunker) running in Electron, where shooting (holding left click + moving mouse) causes network freezes of 100-300ms+.
Downloads
Pre-built patched binaries are available on the Releases page:
Windows x64
| File | Electron Version | Size |
|---|---|---|
electron-v40.6.1-release-patched-win32-x64.zip |
v40.6.1 (latest stable) | 133MB |
electron-v42.0.0-nightly-release-patched-win32-x64.zip |
v42.0.0-nightly | 137MB |
Linux x64
| File | Electron Version | Size |
|---|---|---|
electron-v43.0.0-nightly-release-patched-linux-x64.zip |
v43.0.0-nightly | 113MB |
All builds are full release builds (is_official_build = true) with maximum optimizations. Additional versions can be built from source -- see BUILD-GUIDE.md.
The Problem
When an Electron app uses these flags (common for competitive gaming):
app.commandLine.appendSwitch('disable-frame-rate-limit');
app.commandLine.appendSwitch('disable-gpu-vsync');
...continuous mouse input with left-click held down causes WebSocket onmessage callbacks to be delayed by 100-300ms+. This manifests as network "freezing" -- player positions stop updating, hit registration breaks, and the game becomes unplayable during gunfights.
Root Cause
Three factors in Chromium's Blink main thread scheduler combine to create the issue:
-
Input tasks at
kHighestPriority(priority level 1) while WebSocket/Worker tasks sit atkNormalPriority(level 7) -
No cross-priority anti-starvation in
task_queue_selector.cc-- it always picks from the highest active priority queue, with no mechanism to let lower-priority tasks run -
Compositor priority boost during input -- when mouse is held + moving (
UseCase::kMainThreadCustomInputHandling), the compositor queue gets boosted tokHighestPriority. With--disable-frame-rate-limit, theBackToBackBeginFrameSourcepostsSEND_BEGIN_MAIN_FRAMEtasks at zero delay, creating an infinite loop of highest-priority tasks that permanently starves everything else
This regression was introduced in Chromium 84 when PrioritizeCompositingAfterInput was made unconditional (CL 2132022).
The Fix
Two changes in one file (main_thread_scheduler_impl.cc):
- Lower input task priority from
kHighestPrioritytokNormalPriority - Cap compositor priority to
kNormalPriorityviastd::max()
See patches/ws-priority-patch.diff for the exact diff.
Test Results
12-second automated stress test with continuous CDP mouse input (left-click held, circular movement):
| Build | p99 Latency | Max Latency | Messages >50ms | Mouse Events | Frames |
|---|---|---|---|---|---|
| Unpatched | ~97ms | ~308ms | 8.6% | ~11,380 | ~6,300 |
| Patched | ~34ms | ~38ms | 0% | ~12,360 | ~7,620 |
The patch not only eliminates starvation but actually improves both input throughput (+9% mouse events) and frame rate (+21% frames) because it prevents the input/compositor priority cascade from monopolizing the main thread.
Usage
Option A: Direct Binary
Extract the zip and run your app:
Windows:
electron.exe path/to/your/app
Linux:
./electron path/to/your/app
Option B: Replace in node_modules
# Back up original
mv node_modules/electron/dist node_modules/electron/dist-original
# Extract patched version
mkdir node_modules/electron/dist
cd node_modules/electron/dist
unzip path/to/electron-v40.6.1-release-patched-win32-x64.zip
Option C: electron-builder
In package.json:
{
"build": {
"electronDist": "path/to/extracted/dist",
"electronVersion": "40.6.1"
}
}
Option D: electron-forge
In forge.config.js:
module.exports = {
packagerConfig: {
electronZipDir: 'path/to/extracted/dist'
}
};
Verification
An automated stress test is included in the test/ directory:
cd test
npm install
# Windows
path/to/patched/electron.exe cdp-test.js 8085 PATCHED
# Linux
path/to/patched/electron cdp-test.js 8085 PATCHED
The test uses CDP Input.dispatchMouseEvent to simulate continuous mouse input (the only reliable automated method -- Electron's sendInputEvent API bypasses the compositor thread and doesn't trigger the bug).
Expected output for a patched build: 0% of WebSocket messages >50ms.
Building From Source
Full build instructions are in BUILD-GUIDE.md.
Quick summary:
# 1. Set up environment (depot_tools, build toolchain, Python, etc.)
# 2. Initialize and sync Electron source
e init --root=$HOME/electron my-build --import release # Linux
e init --root=C:\electron my-build --import release # Windows
e sync
# 3. Check out desired version
cd src/electron && git checkout v40.6.1
cd .. && gclient sync --with_branch_heads --with_tags
# 4. Apply patch
git apply path/to/ws-priority-patch.diff
# 5. Configure and build
mkdir -p out/Release
# Set out/Release/args.gn:
# import("//electron/build/args/release.gn")
# is_official_build = true
# use_remoteexec = false
# use_reclient = false
buildtools/linux64/gn gen out/Release # Linux
buildtools/win/gn.exe gen out/Release # Windows
ninja -C out/Release electron
ninja -C out/Release electron:electron_dist_zip
Expect 6-10+ hours for a full build on a modern machine (24 cores, 64GB RAM).
Patch Details
The patch modifies Chromium source (not Electron source), so it applies to any Electron version since Electron 10 (Chromium 84+) on any platform (Windows, Linux, macOS). Line numbers may shift between versions but the function names remain the same:
ComputePriority()-- search forPrioritisationType::kInputComputeCompositorPriority()-- search for that function name
File: third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc
License
The patch itself is provided as-is. Electron is MIT licensed. Chromium is BSD licensed.