import { ReactNode } from 'preact/compat'; import { useContext, useEffect, useState } from 'preact/hooks'; import { getIsProduction } from '~core/index'; import { iife } from '~core/notifications/performance-utils'; import { cn } from '~web/utils/helpers'; import { InteractionEvent, NotificationEvent, getTotalTime, useNotificationsContext, } from './data'; import { getLLMPrompt } from './optimize'; import { ToolbarElementContext } from '~web/widget'; type BaseTimeDataItem = { name: string; time: number; color: string; kind: | 'other-not-javascript' | 'other-javascript' | 'render' | 'other-frame-drop' | 'total-processing-time'; }; type TimeData = Array; const getTimeData = ( selectedEvent: NotificationEvent, isProduction: boolean, ) => { switch (selectedEvent.kind) { // todo: push instead of conditional spread case 'dropped-frames': { const timeData: TimeData = [ ...(isProduction ? [ { name: 'Total Processing Time', time: getTotalTime(selectedEvent.timing), color: 'bg-red-500', kind: 'total-processing-time' as const, }, ] : [ { name: 'Renders', time: selectedEvent.timing.renderTime, color: 'bg-purple-500', kind: 'render' as const, }, { name: 'JavaScript, DOM updates, Draw Frame', time: selectedEvent.timing.otherTime, color: 'bg-[#4b4b4b]', kind: 'other-frame-drop' as const, }, ]), ]; return timeData; } case 'interaction': { const timeData: TimeData = [ ...(!isProduction ? [ { name: 'Renders', time: selectedEvent.timing.renderTime, color: 'bg-purple-500', kind: 'render' as const, }, ] : []), { name: isProduction ? 'React Renders, Hooks, Other JavaScript' : 'JavaScript/React Hooks ', time: selectedEvent.timing.otherJSTime, color: 'bg-[#EFD81A]', kind: 'other-javascript', }, { name: 'Update DOM and Draw New Frame', time: getTotalTime(selectedEvent.timing) - selectedEvent.timing.renderTime - selectedEvent.timing.otherJSTime, color: 'bg-[#1D3A66]', kind: 'other-not-javascript', }, ]; return timeData; } } }; export const OtherVisualization = ({ selectedEvent, }: { selectedEvent: NotificationEvent; }) => { const [isProduction] = useState(getIsProduction() ?? false); const { notificationState } = useNotificationsContext(); const [expandedItems, setExpandedItems] = useState( notificationState.routeMessage?.name ? [notificationState.routeMessage.name] : [], ); const timeData = getTimeData(selectedEvent, isProduction); const root = useContext(ToolbarElementContext); // for when a user clicks a bar of a non render, and gets sent to the other visualization and passes a route message on the way // oxlint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (notificationState.routeMessage?.name) { const container = root?.querySelector('#overview-scroll-container'); const element = root?.querySelector( `#react-scan-overview-bar-${notificationState.routeMessage.name}`, ) as HTMLElement; if (container && element) { const elementTop = element.getBoundingClientRect().top; const containerTop = container.getBoundingClientRect().top; const scrollOffset = elementTop - containerTop; container.scrollTop = container.scrollTop + scrollOffset; } } }, [notificationState.route]); // oxlint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (notificationState.route === 'other-visualization') { setExpandedItems((prev) => notificationState.routeMessage?.name ? [notificationState.routeMessage.name] : prev, ); } }, [notificationState.route]); const totalTime = timeData.reduce((acc, item) => acc + item.time, 0); return (

What was time spent on?

Total: {totalTime.toFixed(0)}ms
{timeData.map((entry) => { const isExpanded = expandedItems.includes(entry.kind); return (
{isExpanded && (

{iife(() => { switch (selectedEvent.kind) { case 'interaction': { switch (entry.kind) { case 'render': { return ( ); } case 'other-javascript': { return ( ); } case 'other-not-javascript': { return ( ); } } } case 'dropped-frames': { switch (entry.kind) { case 'total-processing-time': { return ( ); } case 'render': { return ( <> b.totalTime - a.totalTime, ) .slice(0, 3) .map((render) => ({ name: render.name, percentage: render.totalTime / getTotalTime( selectedEvent.timing, ), })), }, }} /> ); } case 'other-frame-drop': { return ( ); } } } } })}

)}
); })}
); }; type OverviewInput = | { kind: 'js-explanation-base'; } | { kind: 'total-processing'; data: { time: number; }; } | { kind: 'high-render-count-high-js'; data: { renderCount: number; topByCount: Array<{ name: string; count: number }>; }; } | { kind: 'low-render-count-high-js'; data: { renderCount: number; }; } | { kind: 'high-render-count-update-dom-draw-frame'; data: { count: number; percentageOfTotal: number; copyButton: ReactNode; }; } | { kind: 'update-dom-draw-frame'; data: { copyButton: ReactNode; }; } | { kind: 'render'; data: { topByTime: Array<{ name: string; percentage: number }> }; } | { kind: 'other'; }; export const getTotalProcessingTimeInput = (event: NotificationEvent) => { return { kind: 'total-processing', data: { time: getTotalTime(event.timing), }, } satisfies OverviewInput; }; const getDrawInput = (event: InteractionEvent): OverviewInput => { const renderCount = event.groupedFiberRenders.reduce( (prev, curr) => prev + curr.count, 0, ); const renderTime = event.timing.renderTime; const totalTime = getTotalTime(event.timing); const renderPercentage = (renderTime / totalTime) * 100; if (renderCount > 100) { return { kind: 'high-render-count-update-dom-draw-frame', data: { count: renderCount, percentageOfTotal: renderPercentage, copyButton: , }, }; } return { kind: 'update-dom-draw-frame', data: { copyButton: , }, }; }; const CopyPromptButton = () => { const [copying, setCopying] = useState(false); const { notificationState } = useNotificationsContext(); return ( ); }; const getRenderInput = (event: InteractionEvent): OverviewInput => { if (event.timing.renderTime / getTotalTime(event.timing) > 0.3) { return { kind: 'render', data: { topByTime: event.groupedFiberRenders .toSorted((a, b) => b.totalTime - a.totalTime) .slice(0, 3) .map((e) => ({ percentage: e.totalTime / getTotalTime(event.timing), name: e.name, })), }, }; } return { kind: 'other', }; }; const getJSInput = (event: InteractionEvent): OverviewInput => { const renderCount = event.groupedFiberRenders.reduce( (prev, curr) => prev + curr.count, 0, ); if (event.timing.otherJSTime / getTotalTime(event.timing) < 0.2) { return { kind: 'js-explanation-base', }; } if ( event.groupedFiberRenders.find((render) => render.count > 200) || event.groupedFiberRenders.reduce((prev, curr) => prev + curr.count, 0) > 500 ) { // not sure a great heuristic for picking the render count return { kind: 'high-render-count-high-js', data: { renderCount, topByCount: event.groupedFiberRenders .filter((groupedRender) => groupedRender.count > 100) .toSorted((a, b) => b.count - a.count) .slice(0, 3), }, }; } if (event.timing.otherJSTime / getTotalTime(event.timing) > 0.3) { if (event.timing.renderTime > 0.2) { return { kind: 'js-explanation-base', }; } return { kind: 'low-render-count-high-js', data: { renderCount, }, }; } return { kind: 'js-explanation-base', }; }; const Explanation = ({ input }: { input: OverviewInput }) => { switch (input.kind) { case 'total-processing': { return (

This is the time it took to draw the entire frame that was presented to the user. To be at 60FPS, this number needs to be {'<=16ms'}

To debug the issue, check the "Ranked" tab to see if there are significant component renders

On a production React build, React Scan can't access the time it took for component to render. To get that information, run React Scan on a development build

To understand precisely what caused the slowdown while in production, use the Chrome profiler and analyze the function call times.

); } case 'render': { return (

This is the time it took React to run components, and internal logic to handle the output of your component.

The slowest components for this time period were:

{input.data.topByTime.map((item) => (
{item.name}:{' '} {(item.percentage * 100).toFixed(0)}% of total
))}

To view the render times of all your components, and what caused them to render, go to the "Ranked" tab

The "Ranked" tab shows the render times of every component.

The render times of the same components are grouped together into one bar.

Clicking the component will show you what props, state, or context caused the component to re-render.

); } case 'js-explanation-base': { return (

This is the period when JavaScript hooks and other JavaScript outside of React Renders run.

The most common culprit for high JS time is expensive hooks, like expensive callbacks inside of useEffect's or a large number of useEffect's called, but this can also be JavaScript event handlers ('onclick', 'onchange') that performed expensive computation.

If you have lots of components rendering that call hooks, like useEffect, it can add significant overhead even if the callbacks are not expensive. If this is the case, you can try optimizing the renders of those components to avoid the hook from having to run.

You should profile your app using the{' '} Chrome DevTools profiler to learn exactly which functions took the longest to execute.

); } case 'high-render-count-high-js': { return (

This is the period when JavaScript hooks and other JavaScript outside of React Renders run.

{input.data.renderCount === 0 ? ( <>

There were no renders, which means nothing related to React caused this slowdown. The most likely cause of the slowdown is a slow JavaScript event handler, or code related to a Web API

You should try to reproduce the slowdown while profiling your website with the Chrome DevTools profiler to see exactly what functions took the longest to execute.

) : ( <> {' '}

There were {input.data.renderCount} renders, which could have contributed to the high JavaScript/Hook time if they ran lots of hooks, like useEffects.

You should try optimizing the renders of:

{input.data.topByCount.map((item) => (
- {item.name} (rendered {item.count}x)
))}
and then checking if the problem still exists.

You can also try profiling your app using the{' '} Chrome DevTools profiler to see exactly what functions took the longest to execute.

)}
); } case 'low-render-count-high-js': { return (

This is the period when JavaScript hooks and other JavaScript outside of React Renders run.

There were only {input.data.renderCount} renders detected, which means either you had very expensive hooks like{' '} useEffect/useLayoutEffect, or there is other JavaScript running during this interaction that took up the majority of the time.

To understand precisely what caused the slowdown, use the{' '} Chrome profiler and analyze the function call times.

); } case 'high-render-count-update-dom-draw-frame': { return (

These are the calculations the browser is forced to do in response to the JavaScript that ran during the interaction.

This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations.

During this interaction, there were{' '} {input.data.count} renders, which was{' '} {input.data.percentageOfTotal.toFixed(0)}% of the time spent processing

The work performed as a result of the renders may have forced the browser to spend a lot of time to draw the next frame.

You can try optimizing the renders to see if the performance problem still exists using the "Ranked" tab.

If you use an AI-based code editor, you can export the performance data collected as a prompt.

{input.data.copyButton}

Provide this formatted data to the model and ask it to find, or fix, what could be causing this performance problem.

For a larger selection of prompts, try the "Prompts" tab

); } case 'update-dom-draw-frame': { return (

These are the calculations the browser is forced to do in response to the JavaScript that ran during the interaction.

This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations.

If you use an AI-based code editor, you can export the performance data collected as a prompt.

{input.data.copyButton}

Provide this formatted data to the model and ask it to find, or fix, what could be causing this performance problem.

For a larger selection of prompts, try the "Prompts" tab

); } case 'other': { return (

This is the time it took to run everything other than React renders. This can be hooks like useEffect, other JavaScript not part of React, or work the browser has to do to update the DOM and draw the next frame.

To get a better picture of what happened, profile your app using the{' '} Chrome profiler when the performance problem arises.

); } } };