Skip to content

Commit 65571be

Browse files
authored
Load exploration only when in viewport (#6296)
* Load exploration only when in viewport and also hide "1 step after" when initial loading is in progress * Format * Don't expand first column's width briefly on initial loading
1 parent 64d9ead commit 65571be

1 file changed

Lines changed: 114 additions & 100 deletions

File tree

assets/js/dashboard/extra/exploration.js

Lines changed: 114 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, {
55
useRef,
66
useCallback
77
} from 'react'
8+
import LazyLoader from '../components/lazy-loader'
89
import * as api from '../api'
910
import * as url from '../util/url'
1011
import { Tooltip } from '../util/tooltip'
@@ -544,6 +545,8 @@ function columnHeader(index, direction) {
544545
export function FunnelExploration() {
545546
const site = useSiteContext()
546547
const { dashboardState } = useDashboardStateContext()
548+
const [inViewport, setInViewport] = useState(false)
549+
547550
const [steps, setSteps] = useState([])
548551
const [direction, setDirection] = useState(EXPLORATION_DIRECTIONS.FORWARD)
549552
const [funnel, setFunnel] = useState([])
@@ -681,6 +684,8 @@ export function FunnelExploration() {
681684
// On subsequent renders (via user interaction) fetch next steps and,
682685
// if the journey changed, also refetch the funnel.
683686
useEffect(() => {
687+
if (!inViewport) return
688+
684689
const journeyChanged =
685690
prevStepsRef.current !== steps ||
686691
prevDirectionRef.current !== direction ||
@@ -781,9 +786,12 @@ export function FunnelExploration() {
781786
return () => {
782787
cancelled = true
783788
}
784-
}, [site, dashboardState, steps, direction, activeColumnFilter])
789+
}, [site, dashboardState, steps, direction, activeColumnFilter, inViewport])
785790

786-
const numColumns = Math.max(steps.length + 1, 3)
791+
const initialLoading =
792+
!inViewport || (steps.length === 0 && activeColumnLoading)
793+
const numColumns = Math.max(steps.length + 1, initialLoading ? 1 : 3)
794+
const gridColumns = Math.max(numColumns, 3)
787795
const activeColumnIndex = steps.length
788796
const containerRef = useRef(null)
789797

@@ -818,109 +826,115 @@ export function FunnelExploration() {
818826
}, [steps.length])
819827

820828
return (
821-
<div className="flex flex-col gap-4 pt-4">
822-
<div className="flex flex-wrap items-center gap-x-3">
823-
<h4 className="flex-1 text-base font-semibold dark:text-gray-100">
824-
{funnel.length >= 2
825-
? `${funnel.length}-step user journey`
826-
: 'Explore user journeys'}
827-
</h4>
828-
{overallConversionRate != null && (
829-
<div className="order-last sm:order-none w-full sm:w-auto flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
830-
<span>
831-
<span className="font-medium sm:font-semibold text-gray-700 dark:text-gray-200">
832-
Conversion: {parseFloat(overallConversionRate).toFixed(1)}%{' '}
829+
<LazyLoader onVisible={() => setInViewport(true)}>
830+
<div className="flex flex-col gap-4 pt-4">
831+
<div className="flex flex-wrap items-center gap-x-3">
832+
<h4 className="flex-1 text-base font-semibold dark:text-gray-100">
833+
{funnel.length >= 2
834+
? `${funnel.length}-step user journey`
835+
: 'Explore user journeys'}
836+
</h4>
837+
{overallConversionRate != null && (
838+
<div className="order-last sm:order-none w-full sm:w-auto flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
839+
<span>
840+
<span className="font-medium sm:font-semibold text-gray-700 dark:text-gray-200">
841+
Conversion: {parseFloat(overallConversionRate).toFixed(1)}%{' '}
842+
</span>
843+
<span className="text-gray-500 dark:text-gray-400">
844+
({numberShortFormatter(overallConversionVisitors)})
845+
</span>
833846
</span>
834-
<span className="text-gray-500 dark:text-gray-400">
835-
({numberShortFormatter(overallConversionVisitors)})
847+
<span className="hidden sm:inline text-gray-300 dark:text-gray-600 select-none">
848+
|
836849
</span>
837-
</span>
838-
<span className="hidden sm:inline text-gray-300 dark:text-gray-600 select-none">
839-
|
840-
</span>
841-
</div>
842-
)}
843-
<Tooltip
844-
info={<span className="whitespace-nowrap">Deselect all</span>}
845-
className={steps.length === 0 ? 'invisible pointer-events-none' : ''}
846-
>
847-
<button
848-
onClick={handleReset}
849-
className={`${popover.toggleButton.classNames.rounded} ${popover.toggleButton.classNames.outline} justify-center !h-7 px-1.5`}
850+
</div>
851+
)}
852+
<Tooltip
853+
info={<span className="whitespace-nowrap">Deselect all</span>}
854+
className={
855+
steps.length === 0 ? 'invisible pointer-events-none' : ''
856+
}
850857
>
851-
<RefreshIcon className="size-3.5" />
852-
</button>
853-
</Tooltip>
854-
</div>
858+
<button
859+
onClick={handleReset}
860+
className={`${popover.toggleButton.classNames.rounded} ${popover.toggleButton.classNames.outline} justify-center !h-7 px-1.5`}
861+
>
862+
<RefreshIcon className="size-3.5" />
863+
</button>
864+
</Tooltip>
865+
</div>
855866

856-
<div
857-
ref={containerRef}
858-
className="relative grid gap-6 overflow-x-auto -mx-5 px-5 -mb-3 pb-3 [scrollbar-width:thin] [scrollbar-color:theme(colors.gray.300)_transparent] dark:[scrollbar-color:theme(colors.gray.600)_transparent]"
859-
style={{
860-
gridTemplateColumns: `repeat(${numColumns}, minmax(20rem, 1fr))`
861-
}}
862-
>
863-
{Array.from({ length: numColumns }, (_, i) => {
864-
const isActive = i === activeColumnIndex
865-
const isReachable = steps.length >= i
866-
867-
return (
868-
<ExplorationColumn
869-
key={i}
870-
colIndex={i}
871-
header={columnHeader(i, direction)}
872-
className={
873-
steps.length === 0 && i === 2
874-
? 'sm:hidden'
875-
: steps.length === 0 && i === 1
876-
? 'sm:[grid-column:span_2]'
877-
: undefined
878-
}
879-
active={isReachable}
880-
// Active column gets live results; previously-active (now
881-
// selected) columns get the candidate list that was visible at
882-
// the moment of selection so the user can switch options
883-
// without losing context. Pre-selected columns (e.g. populated
884-
// by interesting-funnel preload) have no frozen results and
885-
// fall back to a single-item display sourced from funnel data.
886-
results={
887-
isActive ? activeColumnResults : frozenColumnResults[i] || []
888-
}
889-
loading={isActive ? activeColumnLoading : false}
890-
selected={steps[i] || null}
891-
selectedVisitors={
892-
provisionalFunnelEntries[i]?.visitors ??
893-
funnel[i]?.visitors ??
894-
null
895-
}
896-
selectedConversionRate={
897-
provisionalFunnelEntries[i]?.conversion_rate ??
898-
funnel[i]?.conversion_rate ??
899-
null
900-
}
901-
maxVisitors={funnel[0]?.visitors ?? null}
902-
onSelect={(selected) => handleSelect(i, selected)}
903-
onFilterChange={isActive ? setActiveColumnFilter : () => {}}
904-
filter={isActive ? activeColumnFilter : ''}
905-
direction={direction}
906-
onDirectionChange={i === 0 ? handleDirectionSelect : undefined}
907-
headerConversionRate={
908-
funnel[i]?.conversion_rate != null
909-
? i === 0
910-
? '100%'
911-
: `${parseFloat(funnel[i].conversion_rate).toFixed(1)}%`
912-
: null
913-
}
914-
/>
915-
)
916-
})}
917-
<PathConnectors
918-
key={connectorsKey}
919-
containerRef={containerRef}
920-
steps={steps}
921-
/>
867+
<div
868+
ref={containerRef}
869+
className="relative grid gap-6 overflow-x-auto -mx-5 px-5 -mb-3 pb-3 [scrollbar-width:thin] [scrollbar-color:theme(colors.gray.300)_transparent] dark:[scrollbar-color:theme(colors.gray.600)_transparent]"
870+
style={{
871+
gridTemplateColumns: `repeat(${gridColumns}, minmax(20rem, 1fr))`
872+
}}
873+
>
874+
{Array.from({ length: numColumns }, (_, i) => {
875+
const isActive = i === activeColumnIndex
876+
const isReachable = steps.length >= i
877+
878+
return (
879+
<ExplorationColumn
880+
key={i}
881+
colIndex={i}
882+
header={columnHeader(i, direction)}
883+
className={
884+
steps.length === 0 && i === 2
885+
? 'sm:hidden'
886+
: steps.length === 0 && i === 1
887+
? 'sm:[grid-column:span_2]'
888+
: undefined
889+
}
890+
active={isReachable}
891+
// Active column gets live results; previously-active (now
892+
// selected) columns get the candidate list that was visible at
893+
// the moment of selection so the user can switch options
894+
// without losing context. Pre-selected columns (e.g. populated
895+
// by interesting-funnel preload) have no frozen results and
896+
// fall back to a single-item display sourced from funnel data.
897+
results={
898+
isActive ? activeColumnResults : frozenColumnResults[i] || []
899+
}
900+
loading={
901+
isActive ? initialLoading || activeColumnLoading : false
902+
}
903+
selected={steps[i] || null}
904+
selectedVisitors={
905+
provisionalFunnelEntries[i]?.visitors ??
906+
funnel[i]?.visitors ??
907+
null
908+
}
909+
selectedConversionRate={
910+
provisionalFunnelEntries[i]?.conversion_rate ??
911+
funnel[i]?.conversion_rate ??
912+
null
913+
}
914+
maxVisitors={funnel[0]?.visitors ?? null}
915+
onSelect={(selected) => handleSelect(i, selected)}
916+
onFilterChange={isActive ? setActiveColumnFilter : () => {}}
917+
filter={isActive ? activeColumnFilter : ''}
918+
direction={direction}
919+
onDirectionChange={i === 0 ? handleDirectionSelect : undefined}
920+
headerConversionRate={
921+
funnel[i]?.conversion_rate != null
922+
? i === 0
923+
? '100%'
924+
: `${parseFloat(funnel[i].conversion_rate).toFixed(1)}%`
925+
: null
926+
}
927+
/>
928+
)
929+
})}
930+
<PathConnectors
931+
key={connectorsKey}
932+
containerRef={containerRef}
933+
steps={steps}
934+
/>
935+
</div>
922936
</div>
923-
</div>
937+
</LazyLoader>
924938
)
925939
}
926940

0 commit comments

Comments
 (0)