diff --git a/packages/k8s-ui/src/components/resources/ResourcesView.tsx b/packages/k8s-ui/src/components/resources/ResourcesView.tsx
index 5dec7cf72..b922bb7b6 100644
--- a/packages/k8s-ui/src/components/resources/ResourcesView.tsx
+++ b/packages/k8s-ui/src/components/resources/ResourcesView.tsx
@@ -1,6 +1,7 @@
import React, { useState, useMemo, useEffect, useCallback, useRef, useContext } from 'react'
import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
+import { useNow } from '../../hooks/useNow'
import { PaneLoader } from '../ui/PaneLoader'
import type { TopPodMetrics, TopNodeMetrics } from '../../types'
import {
@@ -1749,6 +1750,26 @@ function getInitialFiltersFromURL() {
// Sort state type
type SortDirection = 'asc' | 'desc' | null
+/**
+ * "Updated Xs" / "Updated 1m" badge in the toolbar.
+ *
+ * Lives in its own component so the 1Hz `useNow` tick re-renders
+ * only this tiny label, NOT the entire ResourcesView (which is
+ * ~4000 lines and contains a virtualized table). Without this
+ * boundary, every visible row's React render would run once per
+ * second just to advance one label by 1s.
+ */
+function LastUpdatedLabel({ lastUpdated }: { lastUpdated: Date }) {
+ // Read the ticking clock here so only this subtree re-renders.
+ useNow(1000)
+ return (
+
+ )
+}
+
export function ResourcesView({
namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange,
apiResources: apiResourcesProp,
@@ -2624,12 +2645,25 @@ export function ResourcesView({
const [refetch, isRefreshAnimating, refreshPhase] = useRefreshAnimation(() => refetchFn?.())
- // Track last updated time
+ // Track last updated time.
+ //
+ // React Query bumps `dataUpdatedAt` every time it records a successful
+ // fetch — even when the response is byte-identical to what's already
+ // cached and structural sharing returns the same `data` reference.
+ // Mounting / focusing windows / a sibling subscriber issuing the same
+ // queryKey can all trigger a no-op refetch. Resetting the user-visible
+ // "Updated Xs" timer on those events is misleading: it suggests fresh
+ // data arrived when nothing actually changed, and the user reads the
+ // "<1s" jump as evidence that opening a filter drawer triggered a real
+ // network round-trip. Gate the bump on data-reference change so we only
+ // bump when the cache actually mutated.
+ const lastDataRef = useRef(undefined)
useEffect(() => {
- if (dataUpdatedAt) {
+ if (dataUpdatedAt && resources !== lastDataRef.current) {
+ lastDataRef.current = resources
setLastUpdated(new Date(dataUpdatedAt))
}
- }, [dataUpdatedAt])
+ }, [dataUpdatedAt, resources])
// Derive counts — prefer lightweight resourceCounts prop over full query data
const counts = useMemo(() => {
@@ -3468,12 +3502,7 @@ export function ResourcesView({
)}
- {lastUpdated && (
-