Skip to content

Commit 9e5e06c

Browse files
mogelbrodcoppa
andauthored
perf: don't update on scroll/rect changes unless virtualized range changes (#48)
* perf: don't update on scroll unless virtualized range changes Also remove the unnecessary `reversedMeasurements` array and `scrollOffsetPlusOuterSize` variable. * fix: remove no longer used useScroll * style: remove semicolons * fix: recalculate range when size prop changes * fix: Re-attach scroll handlers if parentRef changes Co-authored-by: coppa <[email protected]> Co-authored-by: coppa <[email protected]>
1 parent 170c136 commit 9e5e06c

File tree

3 files changed

+67
-116
lines changed

3 files changed

+67
-116
lines changed

src/index.js

Lines changed: 63 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React from 'react'
22

3-
import useScroll from './useScroll'
43
import useRect from './useRect'
54
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'
65

@@ -18,23 +17,15 @@ export function useVirtual({
1817
}) {
1918
const sizeKey = horizontal ? 'width' : 'height'
2019
const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop'
20+
const latestRef = React.useRef({})
2121

2222
const { [sizeKey]: outerSize } = useRect(parentRef) || {
2323
[sizeKey]: 0,
2424
}
2525

26-
const [scrollOffset, _setScrollOffset] = React.useState(0)
27-
28-
const scrollOffsetPlusOuterSize = scrollOffset + outerSize
29-
30-
useScroll(parentRef, ({ [scrollKey]: newScrollOffset }) => {
31-
_setScrollOffset(newScrollOffset)
32-
})
33-
3426
const defaultScrollToFn = React.useCallback(
3527
offset => {
3628
if (parentRef.current) {
37-
_setScrollOffset(offset)
3829
parentRef.current[scrollKey] = offset
3930
}
4031
},
@@ -52,69 +43,56 @@ export function useVirtual({
5243

5344
const [measuredCache, setMeasuredCache] = React.useState({})
5445

55-
const { measurements, reversedMeasurements } = React.useMemo(() => {
46+
const measurements = React.useMemo(() => {
5647
const measurements = []
57-
const reversedMeasurements = []
58-
59-
for (let i = 0, j = size - 1; i < size; i++, j--) {
48+
for (let i = 0; i < size; i++) {
6049
const measuredSize = measuredCache[i]
6150
const start = measurements[i - 1] ? measurements[i - 1].end : paddingStart
6251
const size =
6352
typeof measuredSize === 'number' ? measuredSize : estimateSize(i)
6453
const end = start + size
65-
const bounds = { index: i, start, size, end }
66-
measurements[i] = {
67-
...bounds,
68-
}
69-
reversedMeasurements[j] = {
70-
...bounds,
71-
}
54+
measurements[i] = { index: i, start, size, end }
7255
}
73-
return { measurements, reversedMeasurements }
56+
return measurements
7457
}, [estimateSize, measuredCache, paddingStart, size])
7558

7659
const totalSize = (measurements[size - 1]?.end || 0) + paddingEnd
7760

78-
let start = React.useMemo(
79-
() =>
80-
reversedMeasurements.reduce(
81-
(last, rowStat) => (rowStat.end >= scrollOffset ? rowStat : last),
82-
reversedMeasurements[0]
83-
),
84-
[reversedMeasurements, scrollOffset]
85-
)
61+
Object.assign(latestRef.current, {
62+
overscan,
63+
measurements,
64+
outerSize,
65+
totalSize,
66+
})
8667

87-
let end = React.useMemo(
88-
() =>
89-
measurements.reduce(
90-
(last, rowStat) =>
91-
rowStat.start <= scrollOffsetPlusOuterSize ? rowStat : last,
92-
measurements[0]
93-
),
94-
[measurements, scrollOffsetPlusOuterSize]
95-
)
68+
const [range, setRange] = React.useState({ start: 0, end: 0 })
9669

97-
let startIndex = start ? start.index : 0
98-
let endIndex = end ? end.index : 0
70+
useIsomorphicLayoutEffect(() => {
71+
const element = parentRef.current
9972

100-
// Always add at least one overscan item, so focus will work
101-
startIndex = Math.max(startIndex - overscan, 0)
102-
endIndex = Math.min(endIndex + overscan, size - 1)
73+
const onScroll = () => {
74+
const scrollOffset = element[scrollKey]
75+
latestRef.current.scrollOffset = scrollOffset
76+
setRange(prevRange => calculateRange(latestRef.current, prevRange))
77+
}
10378

104-
const latestRef = React.useRef({})
79+
// Determine initially visible range
80+
onScroll()
10581

106-
latestRef.current = {
107-
measurements,
108-
outerSize,
109-
scrollOffset,
110-
scrollOffsetPlusOuterSize,
111-
totalSize,
112-
}
82+
element.addEventListener('scroll', onScroll, {
83+
capture: false,
84+
passive: true,
85+
})
86+
87+
return () => {
88+
element.removeEventListener('scroll', onScroll)
89+
}
90+
}, [parentRef.current, scrollKey, size /* required */])
11391

11492
const virtualItems = React.useMemo(() => {
11593
const virtualItems = []
11694

117-
for (let i = startIndex; i <= endIndex; i++) {
95+
for (let i = range.start; i <= range.end; i++) {
11896
const measurement = measurements[i]
11997

12098
const item = {
@@ -143,7 +121,7 @@ export function useVirtual({
143121
}
144122

145123
return virtualItems
146-
}, [startIndex, endIndex, measurements, sizeKey, defaultScrollToFn])
124+
}, [range.start, range.end, measurements, sizeKey, defaultScrollToFn])
147125

148126
const mountedRef = React.useRef()
149127

@@ -156,16 +134,12 @@ export function useVirtual({
156134

157135
const scrollToOffset = React.useCallback(
158136
(toOffset, { align = 'start' } = {}) => {
159-
const {
160-
outerSize,
161-
scrollOffset,
162-
scrollOffsetPlusOuterSize,
163-
} = latestRef.current
137+
const { scrollOffset, outerSize } = latestRef.current
164138

165139
if (align === 'auto') {
166140
if (toOffset <= scrollOffset) {
167141
align = 'start'
168-
} else if (scrollOffset >= scrollOffsetPlusOuterSize) {
142+
} else if (scrollOffset >= scrollOffset + outerSize) {
169143
align = 'end'
170144
} else {
171145
align = 'start'
@@ -185,11 +159,7 @@ export function useVirtual({
185159

186160
const tryScrollToIndex = React.useCallback(
187161
(index, { align = 'auto', ...rest } = {}) => {
188-
const {
189-
measurements,
190-
scrollOffset,
191-
scrollOffsetPlusOuterSize,
192-
} = latestRef.current
162+
const { measurements, scrollOffset, outerSize } = latestRef.current
193163

194164
const measurement = measurements[Math.max(0, Math.min(index, size - 1))]
195165

@@ -198,7 +168,7 @@ export function useVirtual({
198168
}
199169

200170
if (align === 'auto') {
201-
if (measurement.end >= scrollOffsetPlusOuterSize) {
171+
if (measurement.end >= scrollOffset + outerSize) {
202172
align = 'end'
203173
} else if (measurement.start <= scrollOffset) {
204174
align = 'start'
@@ -241,3 +211,30 @@ export function useVirtual({
241211
scrollToIndex,
242212
}
243213
}
214+
215+
function calculateRange({
216+
overscan,
217+
measurements,
218+
outerSize,
219+
scrollOffset,
220+
}, prevRange) {
221+
const total = measurements.length
222+
let start = total - 1
223+
while (start > 0 && measurements[start].end >= scrollOffset) {
224+
start -= 1
225+
}
226+
let end = 0
227+
while (end < total - 1 && measurements[end].start <= scrollOffset + outerSize) {
228+
end += 1
229+
}
230+
231+
// Always add at least one overscan item, so focus will work
232+
start = Math.max(start - overscan, 0)
233+
end = Math.min(end + overscan, total - 1)
234+
235+
if (!prevRange || prevRange.start !== start || prevRange.end !== end) {
236+
return { start, end }
237+
}
238+
239+
return prevRange
240+
}

src/useRect.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'
66

77
export default function useRect(nodeRef) {
88
const [element, setElement] = React.useState(nodeRef.current)
9-
const [rect, dispatch] = React.useReducer(rectReducer, null);
9+
const [rect, dispatch] = React.useReducer(rectReducer, null)
1010
const initialRectSet = React.useRef(false)
1111

1212
useIsomorphicLayoutEffect(() => {
@@ -43,10 +43,10 @@ export default function useRect(nodeRef) {
4343
}
4444

4545
function rectReducer(state, action) {
46-
const rect = action.rect;
46+
const rect = action.rect
4747
if (!state || state.height !== rect.height || state.width !== rect.width) {
48-
return rect;
48+
return rect
4949
}
50-
return state;
50+
return state
5151
}
5252

src/useScroll.js

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)