Add Lazy Loading for Dashboard Components with LazyWhenVisible
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced LazyWhenVisible component to optimize rendering of dashboard sections based on scroll visibility, improving performance.
- Updated DashboardOverviewPage to utilize LazyWhenVisible for sections containing stats, history, or tables, enhancing user experience by loading content only when visible.
- Added useScrollVisibility hook to manage visibility state, ensuring efficient resource usage during scrolling.
This commit is contained in:
Tom Butcher 2026-08-30 14:17:51 +01:00
parent b212331129
commit e3a20e6b48
3 changed files with 101 additions and 4 deletions

View File

@ -16,6 +16,7 @@ import ScrollBox from './ScrollBox'
import InfoCollapse from './InfoCollapse' import InfoCollapse from './InfoCollapse'
import StatsDisplay, { StatsViewButton } from './StatsDisplay' import StatsDisplay, { StatsViewButton } from './StatsDisplay'
import ModelHistoryDisplay from './ModelHistoryDisplay' import ModelHistoryDisplay from './ModelHistoryDisplay'
import LazyWhenVisible from './LazyWhenVisible'
import ObjectTable from './ObjectTable' import ObjectTable from './ObjectTable'
import DashboardOverviewFlexColumns from './DashboardOverviewFlexColumns' import DashboardOverviewFlexColumns from './DashboardOverviewFlexColumns'
import useCollapseState from '../hooks/useCollapseState' import useCollapseState from '../hooks/useCollapseState'
@ -734,12 +735,11 @@ const DashboardOverviewPage = ({
} }
const renderCollapse = (section, dragHandle = null) => { const renderCollapse = (section, dragHandle = null) => {
const statsObjectType = const isStats = section.content?.type === StatsDisplay
section.content?.type === StatsDisplay const statsObjectType = isStats ? section.content.props?.objectType : null
? section.content.props?.objectType
: null
const isHistory = section.content?.type === ModelHistoryDisplay const isHistory = section.content?.type === ModelHistoryDisplay
const isTable = section.content?.type === ObjectTable const isTable = section.content?.type === ObjectTable
const isLazyContent = isStats || isHistory || isTable
const resizable = isHistory || isTable const resizable = isHistory || isTable
const defaultHeight = isHistory const defaultHeight = isHistory
? DEFAULT_HISTORY_HEIGHT ? DEFAULT_HISTORY_HEIGHT
@ -757,6 +757,19 @@ const DashboardOverviewPage = ({
: cloneElement(content, { scrollHeight: `${contentHeight}px` }) : cloneElement(content, { scrollHeight: `${contentHeight}px` })
} }
if (isLazyContent && content) {
content = (
<LazyWhenVisible
scrollRootRef={scrollRef}
minHeight={
isHistory || isTable ? contentHeight : 120
}
>
{content}
</LazyWhenVisible>
)
}
return ( return (
<InfoCollapse <InfoCollapse
title={section.title} title={section.title}

View File

@ -0,0 +1,34 @@
import PropTypes from 'prop-types'
import useScrollVisibility from '../hooks/useScrollVisibility'
const LazyWhenVisible = ({
scrollRootRef,
minHeight,
enabled = true,
children
}) => {
const { targetRef, isVisible } = useScrollVisibility(scrollRootRef, {
enabled
})
if (!enabled || isVisible) {
return children
}
return (
<div
ref={targetRef}
style={minHeight != null ? { minHeight } : undefined}
aria-hidden
/>
)
}
LazyWhenVisible.propTypes = {
scrollRootRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
minHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
enabled: PropTypes.bool,
children: PropTypes.node
}
export default LazyWhenVisible

View File

@ -0,0 +1,50 @@
import { useEffect, useRef, useState } from 'react'
const useScrollVisibility = (
scrollRootRef,
{ rootMargin = '200px 0px', threshold = 0, enabled = true } = {}
) => {
const targetRef = useRef(null)
const [isVisible, setIsVisible] = useState(!enabled)
useEffect(() => {
if (!enabled || isVisible) return undefined
const target = targetRef.current
if (!target) return undefined
let observer
let cancelled = false
const attach = () => {
if (cancelled) return
const root = scrollRootRef?.current
if (!root) {
requestAnimationFrame(attach)
return
}
observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
}
},
{ root, rootMargin, threshold }
)
observer.observe(target)
}
attach()
return () => {
cancelled = true
observer?.disconnect()
}
}, [scrollRootRef, rootMargin, threshold, enabled, isVisible])
return { targetRef, isVisible }
}
export default useScrollVisibility