From 9cb60ab960b7bba484a310342c5ffeb7d047a893 Mon Sep 17 00:00:00 2001 From: Carlos Edu Js Date: Thu, 15 Jan 2026 12:32:41 -0300 Subject: [PATCH 1/2] [DevTools] Fix high CPU usage on pages without React (#35515) Stop polling for React after 10 attempts (~5 seconds) to prevent infinite loop of 'hello' messages on pages without React. Fixes #35515 --- .../react-devtools-extensions/src/contentScripts/proxy.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/react-devtools-extensions/src/contentScripts/proxy.js b/packages/react-devtools-extensions/src/contentScripts/proxy.js index a4e7dd68241..5d4a8eb67e5 100644 --- a/packages/react-devtools-extensions/src/contentScripts/proxy.js +++ b/packages/react-devtools-extensions/src/contentScripts/proxy.js @@ -22,11 +22,19 @@ function injectProxy({target}: {target: any}) { // The backend waits to install the global hook until notified by the content script. // In the event of a page reload, the content script might be loaded before the backend manager is injected. // Because of this we need to poll the backend manager until it has been initialized. + // We limit retries to avoid high CPU usage on pages without React (see issue #35515). + let retryCount = 0; + const maxRetries = 10; // Stop polling after ~5 seconds (10 * 500ms) + const intervalID: IntervalID = setInterval(() => { if (backendInitialized) { clearInterval(intervalID); + } else if (retryCount >= maxRetries) { + // Stop polling on pages without React to prevent high CPU usage + clearInterval(intervalID); } else { sayHelloToBackendManager(); + retryCount++; } }, 500); } From 11f5691d48f95eb346fade4f1f014bf8a6a2efa3 Mon Sep 17 00:00:00 2001 From: Carlos Edu Js Date: Thu, 15 Jan 2026 23:34:27 -0300 Subject: [PATCH 2/2] [DevTools] Refactor handshake to be event-driven (#35515) Replaces the polling mechanism in proxy.js with an event-driven approach. backendManager.js now dispatches a 'react-devtools-ready' event and sets a data attribute when initialized. This eliminates CPU usage on non-React pages by removing the need for a polling loop. --- .../src/contentScripts/backendManager.js | 8 +++++ .../src/contentScripts/proxy.js | 31 +++++++++---------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/react-devtools-extensions/src/contentScripts/backendManager.js b/packages/react-devtools-extensions/src/contentScripts/backendManager.js index 402a1378576..8969bf7bedf 100644 --- a/packages/react-devtools-extensions/src/contentScripts/backendManager.js +++ b/packages/react-devtools-extensions/src/contentScripts/backendManager.js @@ -207,4 +207,12 @@ if (!window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__) { window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__ = true; window.addEventListener('message', welcome); + + // Signal to the content script that the backend manager has been injected. + // This allows the content script to connect immediately without polling. + window.document.documentElement.setAttribute( + 'data-react-devtools-ready', + 'true', + ); + window.dispatchEvent(new CustomEvent('react-devtools-ready')); } diff --git a/packages/react-devtools-extensions/src/contentScripts/proxy.js b/packages/react-devtools-extensions/src/contentScripts/proxy.js index 5d4a8eb67e5..e7d79a5f959 100644 --- a/packages/react-devtools-extensions/src/contentScripts/proxy.js +++ b/packages/react-devtools-extensions/src/contentScripts/proxy.js @@ -17,26 +17,23 @@ function injectProxy({target}: {target: any}) { window.__REACT_DEVTOOLS_PROXY_INJECTED__ = true; connectPort(); - sayHelloToBackendManager(); // The backend waits to install the global hook until notified by the content script. // In the event of a page reload, the content script might be loaded before the backend manager is injected. - // Because of this we need to poll the backend manager until it has been initialized. - // We limit retries to avoid high CPU usage on pages without React (see issue #35515). - let retryCount = 0; - const maxRetries = 10; // Stop polling after ~5 seconds (10 * 500ms) - - const intervalID: IntervalID = setInterval(() => { - if (backendInitialized) { - clearInterval(intervalID); - } else if (retryCount >= maxRetries) { - // Stop polling on pages without React to prevent high CPU usage - clearInterval(intervalID); - } else { - sayHelloToBackendManager(); - retryCount++; - } - }, 500); + // Because of this we need to synchronize with the backend manager. + // We use a data attribute and a custom event to detect when the backend manager is ready, + // avoiding the need for CPU-intensive polling (see issue #35515). + + const onBackendReady = () => { + window.removeEventListener('react-devtools-ready', onBackendReady); + sayHelloToBackendManager(); + }; + + if (document.documentElement.getAttribute('data-react-devtools-ready')) { + onBackendReady(); + } else { + window.addEventListener('react-devtools-ready', onBackendReady); + } } }