Repository: facebook/react
Pull request: Use console.timeStamp instead of performance.measure in Component Performance Track

This is a new extension that Chrome added to the existing `console.timeStamp` similar to the extensions added to `performance.measure`. This one should be significantly faster because it doesn't have the extra object indirection, it doesn't return a `PerformanceMeasure` entry and doesn't register itself with the global system of entries.

I also use `performance.measure` in DEV for errors since we can attach the error to the `properties` extension which doesn't exist for `console.timeStamp`.

A downside of using this API is that there's no programmatic API for the site itself to collect its own logs from React. Which the previous allowed us to use the standard `performance.getEntries()` for. The recommendation instead will be for the site to patch `console.timeStamp` if it wants to collect measurements from React just like you're recommended to patch `console.error` or `fetch` or whatever to collect other instrumentation metrics.

This extension works in Chrome canary but it doesn't yet work fully in Chrome stable. We might want to wait until it has propagated to Chrome to stable. It should be in Chrome 136.

Changed files:
filename: packages/react-client/src/ReactFlightPerformanceTrack.js
patch: @@ -7,52 +7,35 @@
  * @flow
  */
 
+/* eslint-disable react-internal/no-production-logging */
+
 import type {ReactComponentInfo} from 'shared/ReactTypes';
 
 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
 
 const supportsUserTiming =
   enableProfilerTimer &&
-  typeof performance !== 'undefined' &&
-  // $FlowFixMe[method-unbinding]
-  typeof performance.measure === 'function';
+  typeof console !== 'undefined' &&
+  typeof console.timeStamp === 'function';
 
 const COMPONENTS_TRACK = 'Server Components ⚛';
 
-const componentsTrackMarker = {
-  startTime: 0.001,
-  detail: {
-    devtools: {
-      color: 'primary-light',
-      track: 'Primary',
-      trackGroup: COMPONENTS_TRACK,
-    },
-  },
-};
-
 export function markAllTracksInOrder() {
   if (supportsUserTiming) {
     // Ensure we create the Server Component track groups earlier than the Client Scheduler
     // and Client Components. We can always add the 0 time slot even if it's in the past.
     // That's still considered for ordering.
-    performance.mark('Server Components Track', componentsTrackMarker);
+    console.timeStamp(
+      'Server Components Track',
+      0.001,
+      0.001,
+      'Primary',
+      COMPONENTS_TRACK,
+      'primary-light',
+    );
   }
 }
 
-// Reused to avoid thrashing the GC.
-const reusableComponentDevToolDetails = {
-  color: 'primary',
-  track: '',
-  trackGroup: COMPONENTS_TRACK,
-};
-const reusableComponentOptions = {
-  start: -0,
-  end: -0,
-  detail: {
-    devtools: reusableComponentDevToolDetails,
-  },
-};
-
 const trackNames = [
   'Primary',
   'Parallel',
@@ -79,7 +62,7 @@ export function logComponentRender(
     const name = componentInfo.name;
     const isPrimaryEnv = env === rootEnv;
     const selfTime = endTime - startTime;
-    reusableComponentDevToolDetails.color =
+    const color =
       selfTime < 0.5
         ? isPrimaryEnv
           ? 'primary-light'
@@ -93,12 +76,16 @@ export function logComponentRender(
               ? 'primary-dark'
               : 'secondary-dark'
             : 'error';
-    reusableComponentDevToolDetails.track = trackNames[trackIdx];
-    reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
-    reusableComponentOptions.end = childrenEndTime;
     const entryName =
       isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
-    performance.measure(entryName, reusableComponentOptions);
+    console.timeStamp(
+      entryName,
+      startTime < 0 ? 0 : startTime,
+      childrenEndTime,
+      trackNames[trackIdx],
+      COMPONENTS_TRACK,
+      color,
+    );
   }
 }
 
@@ -112,8 +99,17 @@ export function logComponentErrored(
   error: mixed,
 ): void {
   if (supportsUserTiming) {
-    const properties = [];
-    if (__DEV__) {
+    const env = componentInfo.env;
+    const name = componentInfo.name;
+    const isPrimaryEnv = env === rootEnv;
+    const entryName =
+      isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
+    if (
+      __DEV__ &&
+      typeof performance !== 'undefined' &&
+      // $FlowFixMe[method-unbinding]
+      typeof performance.measure === 'function'
+    ) {
       const message =
         typeof error === 'object' &&
         error !== null &&
@@ -122,26 +118,30 @@ export function logComponentErrored(
             String(error.message)
           : // eslint-disable-next-line react-internal/safe-string-coercion
             String(error);
-      properties.push(['Error', message]);
-    }
-    const env = componentInfo.env;
-    const name = componentInfo.name;
-    const isPrimaryEnv = env === rootEnv;
-    const entryName =
-      isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
-    performance.measure(entryName, {
-      start: startTime < 0 ? 0 : startTime,
-      end: childrenEndTime,
-      detail: {
-        devtools: {
-          color: 'error',
-          track: trackNames[trackIdx],
-          trackGroup: COMPONENTS_TRACK,
-          tooltipText: entryName + ' Errored',
-          properties,
+      const properties = [['Error', message]];
+      performance.measure(entryName, {
+        start: startTime < 0 ? 0 : startTime,
+        end: childrenEndTime,
+        detail: {
+          devtools: {
+            color: 'error',
+            track: trackNames[trackIdx],
+            trackGroup: COMPONENTS_TRACK,
+            tooltipText: entryName + ' Errored',
+            properties,
+          },
         },
-      },
-    });
+      });
+    } else {
+      console.timeStamp(
+        entryName,
+        startTime < 0 ? 0 : startTime,
+        childrenEndTime,
+        trackNames[trackIdx],
+        COMPONENTS_TRACK,
+        'error',
+      );
+    }
   }
 }
 
@@ -153,11 +153,14 @@ export function logDedupedComponentRender(
 ): void {
   if (supportsUserTiming && endTime >= 0 && trackIdx < 10) {
     const name = componentInfo.name;
-    reusableComponentDevToolDetails.color = 'tertiary-light';
-    reusableComponentDevToolDetails.track = trackNames[trackIdx];
-    reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
-    reusableComponentOptions.end = endTime;
     const entryName = name + ' [deduped]';
-    performance.measure(entryName, reusableComponentOptions);
+    console.timeStamp(
+      entryName,
+      startTime < 0 ? 0 : startTime,
+      endTime,
+      trackNames[trackIdx],
+      COMPONENTS_TRACK,
+      'tertiary-light',
+    );
   }
 }

filename: packages/react-reconciler/src/ReactFiberPerformanceTrack.js
patch: @@ -7,6 +7,8 @@
  * @flow
  */
 
+/* eslint-disable react-internal/no-production-logging */
+
 import type {Fiber} from './ReactInternalTypes';
 
 import type {Lanes} from './ReactFiberLane';
@@ -28,27 +30,14 @@ import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
 
 const supportsUserTiming =
   enableProfilerTimer &&
-  typeof performance !== 'undefined' &&
-  // $FlowFixMe[method-unbinding]
-  typeof performance.measure === 'function';
+  typeof console !== 'undefined' &&
+  typeof console.timeStamp === 'function';
 
 const COMPONENTS_TRACK = 'Components ⚛';
-
-// Reused to avoid thrashing the GC.
-const reusableComponentDevToolDetails = {
-  color: 'primary',
-  track: COMPONENTS_TRACK,
-};
-const reusableComponentOptions = {
-  start: -0,
-  end: -0,
-  detail: {
-    devtools: reusableComponentDevToolDetails,
-  },
-};
-
 const LANES_TRACK_GROUP = 'Scheduler ⚛';
 
+let currentTrack: string = 'Blocking'; // Lane
+
 const reusableLaneDevToolDetails = {
   color: 'primary',
   track: 'Blocking', // Lane
@@ -63,63 +52,47 @@ const reusableLaneOptions = {
 };
 
 export function setCurrentTrackFromLanes(lanes: Lanes): void {
-  reusableLaneDevToolDetails.track = getGroupNameOfHighestPriorityLane(lanes);
+  currentTrack = getGroupNameOfHighestPriorityLane(lanes);
 }
 
-const blockingLaneMarker = {
-  startTime: 0.003,
-  detail: {
-    devtools: {
-      color: 'primary-light',
-      track: 'Blocking',
-      trackGroup: LANES_TRACK_GROUP,
-    },
-  },
-};
-
-const transitionLaneMarker = {
-  startTime: 0.003,
-  detail: {
-    devtools: {
-      color: 'primary-light',
-      track: 'Transition',
-      trackGroup: LANES_TRACK_GROUP,
-    },
-  },
-};
-
-const suspenseLaneMarker = {
-  startTime: 0.003,
-  detail: {
-    devtools: {
-      color: 'primary-light',
-      track: 'Suspense',
-      trackGroup: LANES_TRACK_GROUP,
-    },
-  },
-};
-
-const idleLaneMarker = {
-  startTime: 0.003,
-  detail: {
-    devtools: {
-      color: 'primary-light',
-      track: 'Idle',
-      trackGroup: LANES_TRACK_GROUP,
-    },
-  },
-};
-
 export function markAllLanesInOrder() {
   if (supportsUserTiming) {
     // Ensure we create all tracks in priority order. Currently performance.mark() are in
     // first insertion order but performance.measure() are in the reverse order. We can
     // always add the 0 time slot even if it's in the past. That's still considered for
     // ordering.
-    performance.mark('Blocking Track', blockingLaneMarker);
-    performance.mark('Transition Track', transitionLaneMarker);
-    performance.mark('Suspense Track', suspenseLaneMarker);
-    performance.mark('Idle Track', idleLaneMarker);
+    console.timeStamp(
+      'Blocking Track',
+      0.003,
+      0.003,
+      'Blocking',
+      LANES_TRACK_GROUP,
+      'primary-light',
+    );
+    console.timeStamp(
+      'Transition Track',
+      0.003,
+      0.003,
+      'Transition',
+      LANES_TRACK_GROUP,
+      'primary-light',
+    );
+    console.timeStamp(
+      'Suspense Track',
+      0.003,
+      0.003,
+      'Suspense',
+      LANES_TRACK_GROUP,
+      'primary-light',
+    );
+    console.timeStamp(
+      'Idle Track',
+      0.003,
+      0.003,
+      'Idle',
+      LANES_TRACK_GROUP,
+      'primary-light',
+    );
   }
 }
 
@@ -130,10 +103,14 @@ function logComponentTrigger(
   trigger: string,
 ) {
   if (supportsUserTiming) {
-    reusableComponentDevToolDetails.color = 'warning';
-    reusableComponentOptions.start = startTime;
-    reusableComponentOptions.end = endTime;
-    performance.measure(trigger, reusableComponentOptions);
+    console.timeStamp(
+      trigger,
+      startTime,
+      endTime,
+      COMPONENTS_TRACK,
+      undefined,
+      'warning',
+    );
   }
 }
 
@@ -187,7 +164,7 @@ export function logComponentRender(
         selfTime -= (child.actualDuration: any);
       }
     }
-    reusableComponentDevToolDetails.color =
+    const color =
       selfTime < 0.5
         ? wasHydrated
           ? 'tertiary-light'
@@ -201,9 +178,14 @@ export function logComponentRender(
               ? 'tertiary-dark'
               : 'primary-dark'
             : 'error';
-    reusableComponentOptions.start = startTime;
-    reusableComponentOptions.end = endTime;
-    performance.measure(name, reusableComponentOptions);
+    console.timeStamp(
+      name,
+      startTime,
+      endTime,
+      COMPONENTS_TRACK,
+      undefined,
+      color,
+    );
   }
 }
 
@@ -219,8 +201,13 @@ export function logComponentErrored(
       // Skip
       return;
     }
-    const properties = [];
-    if (__DEV__) {
+    if (
+      __DEV__ &&
+      typeof performance !== 'undefined' &&
+      // $FlowFixMe[method-unbinding]
+      typeof performance.measure === 'function'
+    ) {
+      const properties = [];
       for (let i = 0; i < errors.length; i++) {
         const capturedValue = errors[i];
         const error = capturedValue.value;
@@ -234,22 +221,31 @@ export function logComponentErrored(
               String(error);
         properties.push(['Error', message]);
       }
-    }
-    performance.measure(name, {
-      start: startTime,
-      end: endTime,
-      detail: {
-        devtools: {
-          color: 'error',
-          track: COMPONENTS_TRACK,
-          tooltipText:
-            fiber.tag === SuspenseComponent
-              ? 'Hydration failed'
-              : 'Error boundary caught an error',
-          properties,
+      performance.measure(name, {
+        start: startTime,
+        end: endTime,
+        detail: {
+          devtools: {
+            color: 'error',
+            track: COMPONENTS_TRACK,
+            tooltipText:
+              fiber.tag === SuspenseComponent
+                ? 'Hydration failed'
+                : 'Error boundary caught an error',
+            properties,
+          },
         },
-      },
-    });
+      });
+    } else {
+      console.timeStamp(
+        name,
+        startTime,
+        endTime,
+        COMPONENTS_TRACK,
+        undefined,
+        'error',
+      );
+    }
   }
 }
 
@@ -265,8 +261,13 @@ function logComponentEffectErrored(
       // Skip
       return;
     }
-    const properties = [];
-    if (__DEV__) {
+    if (
+      __DEV__ &&
+      typeof performance !== 'undefined' &&
+      // $FlowFixMe[method-unbinding]
+      typeof performance.measure === 'function'
+    ) {
+      const properties = [];
       for (let i = 0; i < errors.length; i++) {
         const capturedValue = errors[i];
         const error = capturedValue.value;
@@ -280,19 +281,28 @@ function logComponentEffectErrored(
               String(error);
         properties.push(['Error', message]);
       }
-    }
-    performance.measure(name, {
-      start: startTime,
-      end: endTime,
-      detail: {
-        devtools: {
-          color: 'error',
-          track: COMPONENTS_TRACK,
-          tooltipText: 'A lifecycle or effect errored',
-          properties,
+      performance.measure(name, {
+        start: startTime,
+        end: endTime,
+        detail: {
+          devtools: {
+            color: 'error',
+            track: COMPONENTS_TRACK,
+            tooltipText: 'A lifecycle or effect errored',
+            properties,
+          },
         },
-      },
-    });
+      });
+    } else {
+      console.timeStamp(
+        name,
+        startTime,
+        endTime,
+        COMPONENTS_TRACK,
+        undefined,
+        'error',
+      );
+    }
   }
 }
 
@@ -313,17 +323,22 @@ export function logComponentEffect(
     return;
   }
   if (supportsUserTiming) {
-    reusableComponentDevToolDetails.color =
+    const color =
       selfTime < 1
         ? 'secondary-light'
         : selfTime < 100
           ? 'secondary'
           : selfTime < 500
             ? 'secondary-dark'
             : 'error';
-    reusableComponentOptions.start = startTime;
-    reusableComponentOptions.end = endTime;
-    performance.measure(name, reusableComponentOptions);
+    console.timeStamp(
+      name,
+      startTime,
+      endTime,
+      COMPONENTS_TRACK,
+      undefined,
+      color,
+    );
   }
 }
 
@@ -335,21 +350,26 @@ export function logYieldTime(startTime: number, endTime: number): void {
       return;
     }
     // Being blocked on CPU is potentially bad so we color it by how long it took.
-    reusableComponentDevToolDetails.color =
+    const color =
       yieldDuration < 5
         ? 'primary-light'
         : yieldDuration < 10
           ? 'primary'
           : yieldDuration < 100
             ? 'primary-dark'
             : 'error';
-    reusableComponentOptions.start = startTime;
-    reusableComponentOptions.end = endTime;
     // This get logged in the components track if we don't commit which leaves them
     // hanging by themselves without context. It's a useful indicator for why something
     // might be starving this render though.
     // TODO: Considering adding these to a queue and only logging them if we commit.
-    performance.measure('Blocked', reusableComponentOptions);
+    console.timeStamp(
+      'Blocked',
+      startTime,
+      endTime,
+      COMPONENTS_TRACK,
+      undefined,
+      color,
+    );
   }
 }
 
@@ -359,10 +379,14 @@ export function logSuspendedYieldTime(
   suspendedFiber: Fiber,
 ): void {
   if (supportsUserTiming) {
-    reusableComponentDevToolDetails.color = 'primary-light';
-    reusableComponentOptions.start = startTime;
-    reusableComponentOptions.end = endTime;
-    performance.measure('Suspended', reusableComponentOptions);
+    console.timeStamp(
+      'Suspended',
+      startTime,
+      endTime,
+      COMPONENTS_TRACK,
+      undefined,
+      'primary-light',
+    );
   }
 }
 
@@ -372,10 +396,14 @@ export function logActionYieldTime(
   suspendedFiber: Fiber,
 ): void {
   if (supportsUserTiming) {
-    reusableComponentDevToolDetails.color = 'primary-light';
-    reusableComponentOptions.start = startTime;
-    reusableComponentOptions.end = endTime;
-    performance.measure('Action', reusableComponentOptions);
+    console.timeStamp(
+      'Action',
+      startTime,
+      endTime,
+      COMPONENTS_TRACK,
+      undefined,
+      'primary-light',
+    );
   }
 }
 
@@ -389,38 +417,40 @@ export function logBlockingStart(
   lanes: Lanes,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.track = 'Blocking';
+    currentTrack = 'Blocking';
     // If a blocking update was spawned within render or an effect, that's considered a cascading render.
     // If you have a second blocking update within the same event, that suggests multiple flushSync or
     // setState in a microtask which is also considered a cascade.
     if (eventTime > 0 && eventType !== null) {
       // Log the time from the event timeStamp until we called setState.
-      reusableLaneDevToolDetails.color = eventIsRepeat
-        ? 'secondary-light'
-        : 'warning';
-      reusableLaneOptions.start = eventTime;
-      reusableLaneOptions.end = updateTime > 0 ? updateTime : renderStartTime;
-      performance.measure(
+      const color = eventIsRepeat ? 'secondary-light' : 'warning';
+      console.timeStamp(
         eventIsRepeat ? '' : 'Event: ' + eventType,
-        reusableLaneOptions,
+        eventTime,
+        updateTime > 0 ? updateTime : renderStartTime,
+        currentTrack,
+        LANES_TRACK_GROUP,
+        color,
       );
     }
     if (updateTime > 0) {
       // Log the time from when we called setState until we started rendering.
-      reusableLaneDevToolDetails.color = isSpawnedUpdate
+      const color = isSpawnedUpdate
         ? 'error'
         : includesOnlyHydrationOrOffscreenLanes(lanes)
           ? 'tertiary-light'
           : 'primary-light';
-      reusableLaneOptions.start = updateTime;
-      reusableLaneOptions.end = renderStartTime;
-      performance.measure(
+      console.timeStamp(
         isSpawnedUpdate
           ? 'Cascading Update'
           : renderStartTime - updateTime > 5
             ? 'Update Blocked'
             : 'Update',
-        reusableLaneOptions,
+        updateTime,
+        renderStartTime,
+        currentTrack,
+        LANES_TRACK_GROUP,
+        color,
       );
     }
   }
@@ -435,39 +465,45 @@ export function logTransitionStart(
   renderStartTime: number,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.track = 'Transition';
+    currentTrack = 'Transition';
     if (eventTime > 0 && eventType !== null) {
       // Log the time from the event timeStamp until we started a transition.
-      reusableLaneDevToolDetails.color = eventIsRepeat
-        ? 'secondary-light'
-        : 'warning';
-      reusableLaneOptions.start = eventTime;
-      reusableLaneOptions.end =
+      const color = eventIsRepeat ? 'secondary-light' : 'warning';
+      const endTime =
         startTime > 0
           ? startTime
           : updateTime > 0
             ? updateTime
             : renderStartTime;
-      performance.measure(
+      console.timeStamp(
         eventIsRepeat ? '' : 'Event: ' + eventType,
-        reusableLaneOptions,
+        eventTime,
+        endTime,
+        currentTrack,
+        LANES_TRACK_GROUP,
+        color,
       );
     }
     if (startTime > 0) {
       // Log the time from when we started an async transition until we called setState or started rendering.
-      reusableLaneDevToolDetails.color = 'primary-dark';
-      reusableLaneOptions.start = startTime;
-      reusableLaneOptions.end = updateTime > 0 ? updateTime : renderStartTime;
-      performance.measure('Action', reusableLaneOptions);
+      console.timeStamp(
+        'Action',
+        startTime,
+        updateTime > 0 ? updateTime : renderStartTime,
+        currentTrack,
+        LANES_TRACK_GROUP,
+        'primary-dark',
+      );
     }
     if (updateTime > 0) {
       // Log the time from when we called setState until we started rendering.
-      reusableLaneDevToolDetails.color = 'primary-light';
-      reusableLaneOptions.start = updateTime;
-      reusableLaneOptions.end = renderStartTime;
-      performance.measure(
+      console.timeStamp(
         renderStartTime - updateTime > 5 ? 'Update Blocked' : 'Update',
-        reusableLaneOptions,
+        updateTime,
+        renderStartTime,
+        currentTrack,
+        LANES_TRACK_GROUP,
+        'primary-light',
       );
     }
   }
@@ -479,20 +515,20 @@ export function logRenderPhase(
   lanes: Lanes,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
-      lanes,
-    )
+    const color = includesOnlyHydrationOrOffscreenLanes(lanes)
       ? 'tertiary-dark'
       : 'primary-dark';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure(
+    console.timeStamp(
       includesOnlyOffscreenLanes(lanes)
         ? 'Prepared'
         : includesOnlyHydrationLanes(lanes)
           ? 'Hydrated'
           : 'Render',
-      reusableLaneOptions,
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      color,
     );
   }
 }
@@ -503,20 +539,20 @@ export function logInterruptedRenderPhase(
   lanes: Lanes,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
-      lanes,
-    )
+    const color = includesOnlyHydrationOrOffscreenLanes(lanes)
       ? 'tertiary-dark'
       : 'primary-dark';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure(
+    console.timeStamp(
       includesOnlyOffscreenLanes(lanes)
         ? 'Prewarm'
         : includesOnlyHydrationLanes(lanes)
           ? 'Interrupted Hydration'
           : 'Interrupted Render',
-      reusableLaneOptions,
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      color,
     );
   }
 }
@@ -527,14 +563,17 @@ export function logSuspendedRenderPhase(
   lanes: Lanes,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
-      lanes,
-    )
+    const color = includesOnlyHydrationOrOffscreenLanes(lanes)
       ? 'tertiary-dark'
       : 'primary-dark';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure('Prewarm', reusableLaneOptions);
+    console.timeStamp(
+      'Prewarm',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      color,
+    );
   }
 }
 
@@ -545,14 +584,17 @@ export function logSuspendedWithDelayPhase(
 ): void {
   // This means the render was suspended and cannot commit until it gets unblocked.
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
-      lanes,
-    )
+    const color = includesOnlyHydrationOrOffscreenLanes(lanes)
       ? 'tertiary-dark'
       : 'primary-dark';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure('Suspended', reusableLaneOptions);
+    console.timeStamp(
+      'Suspended',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      color,
+    );
   }
 }
 
@@ -564,8 +606,13 @@ export function logRecoveredRenderPhase(
   hydrationFailed: boolean,
 ): void {
   if (supportsUserTiming) {
-    const properties = [];
-    if (__DEV__) {
+    if (
+      __DEV__ &&
+      typeof performance !== 'undefined' &&
+      // $FlowFixMe[method-unbinding]
+      typeof performance.measure === 'function'
+    ) {
+      const properties = [];
       for (let i = 0; i < recoverableErrors.length; i++) {
         const capturedValue = recoverableErrors[i];
         const error = capturedValue.value;
@@ -579,22 +626,31 @@ export function logRecoveredRenderPhase(
               String(error);
         properties.push(['Recoverable Error', message]);
       }
-    }
-    performance.measure('Recovered', {
-      start: startTime,
-      end: endTime,
-      detail: {
-        devtools: {
-          color: 'primary-dark',
-          track: reusableLaneDevToolDetails.track,
-          trackGroup: LANES_TRACK_GROUP,
-          tooltipText: hydrationFailed
-            ? 'Hydration Failed'
-            : 'Recovered after Error',
-          properties,
+      performance.measure('Recovered', {
+        start: startTime,
+        end: endTime,
+        detail: {
+          devtools: {
+            color: 'primary-dark',
+            track: currentTrack,
+            trackGroup: LANES_TRACK_GROUP,
+            tooltipText: hydrationFailed
+              ? 'Hydration Failed'
+              : 'Recovered after Error',
+            properties,
+          },
         },
-      },
-    });
+      });
+    } else {
+      console.timeStamp(
+        'Recovered',
+        startTime,
+        endTime,
+        currentTrack,
+        LANES_TRACK_GROUP,
+        'error',
+      );
+    }
   }
 }
 
@@ -604,10 +660,14 @@ export function logErroredRenderPhase(
   lanes: Lanes,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = 'error';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure('Errored', reusableLaneOptions);
+    console.timeStamp(
+      'Errored',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      'error',
+    );
   }
 }
 
@@ -616,10 +676,14 @@ export function logInconsistentRender(
   endTime: number,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = 'error';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure('Teared Render', reusableLaneOptions);
+    console.timeStamp(
+      'Teared Render',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      'error',
+    );
   }
 }
 
@@ -629,10 +693,14 @@ export function logSuspenseThrottlePhase(
 ): void {
   // This was inside a throttled Suspense boundary commit.
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = 'secondary-light';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure('Throttled', reusableLaneOptions);
+    console.timeStamp(
+      'Throttled',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      'secondary-light',
+    );
   }
 }
 
@@ -642,12 +710,16 @@ export function logSuspendedCommitPhase(
 ): void {
   // This means the commit was suspended on CSS or images.
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = 'secondary-light';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
     // TODO: Include the exact reason and URLs of what resources suspended.
     // TODO: This might also be Suspended while waiting on a View Transition.
-    performance.measure('Suspended on CSS or Images', reusableLaneOptions);
+    console.timeStamp(
+      'Suspended on CSS or Images',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      'secondary-light',
+    );
   }
 }
 
@@ -658,8 +730,13 @@ export function logCommitErrored(
   passive: boolean,
 ): void {
   if (supportsUserTiming) {
-    const properties = [];
-    if (__DEV__) {
+    if (
+      __DEV__ &&
+      typeof performance !== 'undefined' &&
+      // $FlowFixMe[method-unbinding]
+      typeof performance.measure === 'function'
+    ) {
+      const properties = [];
       for (let i = 0; i < errors.length; i++) {
         const capturedValue = errors[i];
         const error = capturedValue.value;
@@ -673,20 +750,31 @@ export function logCommitErrored(
               String(error);
         properties.push(['Error', message]);
       }
-    }
-    performance.measure('Errored', {
-      start: startTime,
-      end: endTime,
-      detail: {
-        devtools: {
-          color: 'error',
-          track: reusableLaneDevToolDetails.track,
-          trackGroup: LANES_TRACK_GROUP,
-          tooltipText: passive ? 'Remaining Effects Errored' : 'Commit Errored',
-          properties,
+      performance.measure('Errored', {
+        start: startTime,
+        end: endTime,
+        detail: {
+          devtools: {
+            color: 'error',
+            track: currentTrack,
+            trackGroup: LANES_TRACK_GROUP,
+            tooltipText: passive
+              ? 'Remaining Effects Errored'
+              : 'Commit Errored',
+            properties,
+          },
         },
-      },
-    });
+      });
+    } else {
+      console.timeStamp(
+        'Errored',
+        startTime,
+        endTime,
+        currentTrack,
+        LANES_TRACK_GROUP,
+        'error',
+      );
+    }
   }
 }
 
@@ -700,10 +788,16 @@ export function logCommitPhase(
     return;
   }
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = 'secondary-dark';
     reusableLaneOptions.start = startTime;
     reusableLaneOptions.end = endTime;
-    performance.measure('Commit', reusableLaneOptions);
+    console.timeStamp(
+      'Commit',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      'secondary-dark',
+    );
   }
 }
 
@@ -713,12 +807,13 @@ export function logPaintYieldPhase(
   delayedUntilPaint: boolean,
 ): void {
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = 'secondary-light';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure(
+    console.timeStamp(
       delayedUntilPaint ? 'Waiting for Paint' : '',
-      reusableLaneOptions,
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      'secondary-light',
     );
   }
 }
@@ -733,9 +828,13 @@ export function logPassiveCommitPhase(
     return;
   }
   if (supportsUserTiming) {
-    reusableLaneDevToolDetails.color = 'secondary-dark';
-    reusableLaneOptions.start = startTime;
-    reusableLaneOptions.end = endTime;
-    performance.measure('Remaining Effects', reusableLaneOptions);
+    console.timeStamp(
+      'Remaining Effects',
+      startTime,
+      endTime,
+      currentTrack,
+      LANES_TRACK_GROUP,
+      'secondary-dark',
+    );
   }
 }

filename: scripts/flow/environment.js
patch: @@ -33,6 +33,46 @@ declare interface ConsoleTask {
   run<T>(f: () => T): T;
 }
 
+declare var console: {
+  assert(condition: mixed, ...data: Array<any>): void,
+  clear(): void,
+  count(label?: string): void,
+  countReset(label?: string): void,
+  debug(...data: Array<any>): void,
+  dir(...data: Array<any>): void,
+  dirxml(...data: Array<any>): void,
+  error(...data: Array<any>): void,
+  _exception(...data: Array<any>): void,
+  group(...data: Array<any>): void,
+  groupCollapsed(...data: Array<any>): void,
+  groupEnd(): void,
+  info(...data: Array<any>): void,
+  log(...data: Array<any>): void,
+  profile(name?: string): void,
+  profileEnd(name?: string): void,
+  table(
+    tabularData:
+      | {[key: string]: any, ...}
+      | Array<{[key: string]: any, ...}>
+      | Array<Array<any>>,
+  ): void,
+  time(label?: string): void,
+  timeEnd(label: string): void,
+  timeStamp(
+    label?: string,
+    start?: string | number,
+    end?: string | number,
+    trackName?: string,
+    trackGroup?: string,
+    color?: string,
+  ): void,
+  timeLog(label?: string, ...data?: Array<any>): void,
+  trace(...data: Array<any>): void,
+  warn(...data: Array<any>): void,
+  createTask(label: string): ConsoleTask,
+  ...
+};
+
 type ScrollTimelineOptions = {
   source: Element,
   axis?: 'block' | 'inline' | 'x' | 'y',