diff --git a/packages/react-dom/src/__tests__/refs-test.js b/packages/react-dom/src/__tests__/refs-test.js
index 7cc764bdefdac..aaa1b0358a122 100644
--- a/packages/react-dom/src/__tests__/refs-test.js
+++ b/packages/react-dom/src/__tests__/refs-test.js
@@ -140,18 +140,18 @@ describe('reactiverefs', () => {
     expectClickLogsLengthToBe(testRefsComponent, 1);
 
     // After clicking the reset, there should still only be one click log ref.
-    testRefsComponent.refs.resetDiv.click();
+    ReactTestUtils.act(() => testRefsComponent.refs.resetDiv.click());
     expectClickLogsLengthToBe(testRefsComponent, 1);
 
     // Begin incrementing clicks (and therefore refs).
-    clickIncrementer.click();
+    ReactTestUtils.act(() => clickIncrementer.click());
     expectClickLogsLengthToBe(testRefsComponent, 2);
 
-    clickIncrementer.click();
+    ReactTestUtils.act(() => clickIncrementer.click());
     expectClickLogsLengthToBe(testRefsComponent, 3);
 
     // Now reset again
-    testRefsComponent.refs.resetDiv.click();
+    ReactTestUtils.act(() => testRefsComponent.refs.resetDiv.click());
     expectClickLogsLengthToBe(testRefsComponent, 1);
   });
 });
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 85d678dec501f..dc9bdc1584c5f 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -661,6 +661,24 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
       root !== workInProgressRoot ||
       expirationTime !== renderExpirationTime
     ) {
+      // Before starting a fresh render, check if we can bailout on the entire tree.
+      // For non-root fibers, this bailout usually happens in the begin phase
+      // of the parent component, but roots are special because they don't
+      // have parents.
+      const remainingExpirationTimeOnRoot =
+        root.current.expirationTime > root.current.childExpirationTime
+          ? root.current.expirationTime
+          : root.current.childExpirationTime;
+      if (remainingExpirationTimeOnRoot < expirationTime) {
+        // We can bailout without entering the work loop.
+        markRootFinishedAtTime(
+          root,
+          expirationTime,
+          remainingExpirationTimeOnRoot,
+        );
+        return null;
+      }
+
       prepareFreshStack(root, expirationTime);
       startWorkOnPendingInteractions(root, expirationTime);
     }
@@ -997,6 +1015,24 @@ function performSyncWorkOnRoot(root) {
       root !== workInProgressRoot ||
       expirationTime !== renderExpirationTime
     ) {
+      // Before starting a fresh render, check if we can bailout on the entire tree.
+      // For non-root fibers, this bailout usually happens in the begin phase
+      // of the parent component, but roots are special because they don't
+      // have parents.
+      const remainingExpirationTimeOnRoot =
+        root.current.expirationTime > root.current.childExpirationTime
+          ? root.current.expirationTime
+          : root.current.childExpirationTime;
+      if (remainingExpirationTimeOnRoot < expirationTime) {
+        // We can bailout without entering the work loop.
+        markRootFinishedAtTime(
+          root,
+          expirationTime,
+          remainingExpirationTimeOnRoot,
+        );
+        return null;
+      }
+
       prepareFreshStack(root, expirationTime);
       startWorkOnPendingInteractions(root, expirationTime);
     }