diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue
index c5155eb1..2886150d 100644
--- a/packages/client/src/App.vue
+++ b/packages/client/src/App.vue
@@ -29,6 +29,7 @@ onRpcConnected(() => {
rpc.value.emit('update-client-state', {
minimizePanelInteractive: devtoolsClientState.value.minimizePanelInteractive,
closeOnOutsideClick: devtoolsClientState.value.interactionCloseOnOutsideClick,
+ highlightComponentTracking: devtoolsClientState.value.highlightComponentTracking,
showFloatingPanel: devtoolsClientState.value.showPanel,
reduceMotion: devtoolsClientState.value.reduceMotion,
})
diff --git a/packages/client/src/composables/state.ts b/packages/client/src/composables/state.ts
index 6196b91f..64bdb7a8 100644
--- a/packages/client/src/composables/state.ts
+++ b/packages/client/src/composables/state.ts
@@ -17,6 +17,7 @@ interface DevtoolsClientState {
scale: number
interactionCloseOnOutsideClick: boolean
showPanel: boolean
+ highlightComponentTracking: boolean
minimizePanelInteractive: number
reduceMotion: boolean
}
@@ -45,6 +46,7 @@ function clientStateFactory(): DevtoolsClientState {
scale: 1,
interactionCloseOnOutsideClick: false,
showPanel: true,
+ highlightComponentTracking: false,
minimizePanelInteractive: 5000,
reduceMotion: false,
}
diff --git a/packages/client/src/pages/settings.vue b/packages/client/src/pages/settings.vue
index 77cdfb34..79b252fe 100644
--- a/packages/client/src/pages/settings.vue
+++ b/packages/client/src/pages/settings.vue
@@ -16,7 +16,7 @@ const hostEnv = useHostEnv()
*/
const enableFeatureSettings = hostEnv === 'iframe' || hostEnv === 'separate-window'
-const { scale, interactionCloseOnOutsideClick, showPanel, minimizePanelInteractive, expandSidebar, scrollableSidebar, reduceMotion } = toRefs(toReactive(devtoolsClientState))
+const { scale, interactionCloseOnOutsideClick, showPanel, minimizePanelInteractive, expandSidebar, scrollableSidebar, reduceMotion, highlightComponentTracking } = toRefs(toReactive(devtoolsClientState))
// #region settings
const scaleOptions = [
@@ -210,6 +210,12 @@ const minimizePanelInteractiveLabel = computed(() => {
+
+
+
+
+ Highlight Component Re Rendering
+
diff --git a/packages/devtools-kit/src/core/component-flash/index.ts b/packages/devtools-kit/src/core/component-flash/index.ts
new file mode 100644
index 00000000..57202c1f
--- /dev/null
+++ b/packages/devtools-kit/src/core/component-flash/index.ts
@@ -0,0 +1,61 @@
+import { ComponentHighLighterOptions, VueAppInstance } from '../../../types'
+import { getComponentBoundingRect } from '../component/state/bounding-rect'
+import { getInstanceName } from '../component/utils'
+
+const CONTAINER_ELEMENT_ID = '__vue-devtools-flash__'
+const REMOVE_DELAY_MS = 1000
+
+const containerStyles = {
+ border: '2px rgba(65, 184, 131, 0.7) solid',
+ position: 'fixed',
+ zIndex: '2147483645',
+ pointerEvents: 'none',
+ borderRadius: '3px',
+ boxSizing: 'border-box',
+ transition: 'none',
+ opacity: '1',
+}
+
+function getStyles(bounds: ComponentHighLighterOptions['bounds']) {
+ return {
+ left: `${Math.round(bounds.left)}px`,
+ top: `${Math.round(bounds.top)}px`,
+ width: `${Math.round(bounds.width)}px`,
+ height: `${Math.round(bounds.height)}px`,
+ } satisfies Partial
+}
+
+function create(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial }) {
+ const containerEl = document.createElement('div')
+ containerEl.id = options?.elementId ?? CONTAINER_ELEMENT_ID
+
+ Object.assign(containerEl.style, {
+ ...containerStyles,
+ ...getStyles(options.bounds),
+ ...options.style,
+ })
+
+ document.body.appendChild(containerEl)
+
+ requestAnimationFrame(() => {
+ containerEl.style.transition = 'opacity 1s'
+ containerEl.style.opacity = '0'
+ })
+
+ clearTimeout((containerEl as any)?._timer);
+ (containerEl as any)._timer = setTimeout(() => {
+ document.body.removeChild(containerEl)
+ }, REMOVE_DELAY_MS)
+
+ return containerEl
+}
+
+export function flashComponent(instance: VueAppInstance) {
+ const bounds = getComponentBoundingRect(instance)
+
+ if (!bounds.width && !bounds.height)
+ return
+
+ const name = getInstanceName(instance)
+ create({ bounds, name })
+}
diff --git a/packages/devtools-kit/src/core/plugin/components.ts b/packages/devtools-kit/src/core/plugin/components.ts
index d2bdbb38..99d1bcb4 100644
--- a/packages/devtools-kit/src/core/plugin/components.ts
+++ b/packages/devtools-kit/src/core/plugin/components.ts
@@ -6,6 +6,7 @@ import { ComponentWalker } from '../../core/component/tree/walker'
import { getAppRecord, getComponentId, getComponentInstance } from '../../core/component/utils'
import { activeAppRecord, devtoolsContext, devtoolsState, DevToolsV6PluginAPIHookKeys } from '../../ctx'
import { hook } from '../../hook'
+import { flashComponent } from '../component-flash'
import { setupBuiltinTimelineLayers } from '../timeline'
import { exposeInstanceToWindow } from '../vm'
@@ -115,6 +116,10 @@ export function createComponentsDevToolsPlugin(app: App): [PluginDescriptor, Plu
}
}
+ if (devtoolsState.flashUpdates) {
+ flashComponent(component)
+ }
+
if (!appRecord)
return
@@ -150,6 +155,10 @@ export function createComponentsDevToolsPlugin(app: App): [PluginDescriptor, Plu
}
}
+ if (devtoolsState.flashUpdates) {
+ flashComponent(component)
+ }
+
if (!appRecord)
return
@@ -166,6 +175,10 @@ export function createComponentsDevToolsPlugin(app: App): [PluginDescriptor, Plu
if (!app || (typeof uid !== 'number' && !uid) || !component)
return
+ if (devtoolsState.flashUpdates) {
+ flashComponent(component)
+ }
+
const appRecord = await getAppRecord(app)
if (!appRecord)
diff --git a/packages/devtools-kit/src/ctx/state.ts b/packages/devtools-kit/src/ctx/state.ts
index 70b48109..8b9de4fd 100644
--- a/packages/devtools-kit/src/ctx/state.ts
+++ b/packages/devtools-kit/src/ctx/state.ts
@@ -16,6 +16,7 @@ export interface DevToolsState {
tabs: CustomTab[]
commands: CustomCommand[]
highPerfModeEnabled: boolean
+ flashUpdates: boolean
devtoolsClientDetected: {
[key: string]: boolean
}
@@ -40,6 +41,7 @@ function initStateFactory() {
tabs: [],
commands: [],
highPerfModeEnabled: true,
+ flashUpdates: true,
devtoolsClientDetected: {},
perfUniqueGroupId: 0,
timelineLayersState: getTimelineLayersStateFromStorage(),
diff --git a/packages/overlay/src/components/FrameBox.vue b/packages/overlay/src/components/FrameBox.vue
index f6a082af..f7d7aabc 100644
--- a/packages/overlay/src/components/FrameBox.vue
+++ b/packages/overlay/src/components/FrameBox.vue
@@ -31,6 +31,7 @@ onRpcSeverReady(() => {
updateState({
minimizePanelInactive: v.minimizePanelInteractive,
closeOnOutsideClick: v.closeOnOutsideClick,
+ highlightComponentTracking: v.highlightComponentTracking,
preferShowFloatingPanel: v.showFloatingPanel,
reduceMotion: v.reduceMotion,
})
diff --git a/packages/overlay/src/composables/state.ts b/packages/overlay/src/composables/state.ts
index 58ab4ba5..49fdea5d 100644
--- a/packages/overlay/src/composables/state.ts
+++ b/packages/overlay/src/composables/state.ts
@@ -15,6 +15,7 @@ interface DevToolsFrameState {
minimizePanelInactive: number
preferShowFloatingPanel: boolean
reduceMotion: boolean
+ highlightComponentTracking: boolean
}
export interface UseFrameStateReturn {
@@ -35,6 +36,7 @@ const state = useLocalStorage('__vue-devtools-frame-state__'
minimizePanelInactive: 5000,
preferShowFloatingPanel: true,
reduceMotion: false,
+ highlightComponentTracking: false,
})
export function useFrameState(): UseFrameStateReturn {