Skip to content

Commit f9c9e1e

Browse files
SergeySergey
authored andcommitted
optimize calendar year scroll rendering
1 parent ce85ed4 commit f9c9e1e

5 files changed

Lines changed: 74 additions & 58 deletions

File tree

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,13 @@
11
import { Text } from '@gravity-ui/uikit';
22
import { CalendarYear as CalendarYearModel, CalendarMonth as CalendarMonthModel } from '@repo/models';
3-
import { ICalendarYear, YearID } from '@repo/types';
43
import { memo, useMemo } from 'react';
54

65
import CalendarMonth from './CalendarMonth';
76

87
import s from './CalendarYear.module.scss';
98

10-
const MAX_YEARS_CACHE_SIZE = 100;
11-
const MAX_MONTHS_CACHE_SIZE = MAX_YEARS_CACHE_SIZE * 12;
12-
13-
const yearModelCache = new Map<string, ICalendarYear>();
149
const monthModelCache = new Map<string, CalendarMonthModel>();
15-
16-
const getYearModel = (yearId: string): CalendarYearModel => {
17-
const existing = yearModelCache.get(yearId);
18-
19-
if (existing) {
20-
return existing;
21-
}
22-
23-
const model = new CalendarYearModel(yearId);
24-
25-
yearModelCache.set(yearId, model);
26-
27-
if (yearModelCache.size > MAX_YEARS_CACHE_SIZE) {
28-
const [oldestKey] = yearModelCache.keys();
29-
30-
yearModelCache.delete(oldestKey);
31-
}
32-
33-
return model;
34-
};
10+
const MAX_MONTHS_CACHE_SIZE = 1200;
3511

3612
const getMonthModel = (monthId: string): CalendarMonthModel => {
3713
const existing = monthModelCache.get(monthId);
@@ -53,8 +29,7 @@ const getMonthModel = (monthId: string): CalendarMonthModel => {
5329
return model;
5430
};
5531

56-
const CalendarYear: React.FC<{ yearId: YearID }> = ({ yearId }) => {
57-
const model = getYearModel(yearId);
32+
const CalendarYear: React.FC<{ model: CalendarYearModel }> = ({ model }) => {
5833
const monthsModels = useMemo(() => model.getMonthsKeys().map(getMonthModel), [model]);
5934

6035
return (
@@ -71,4 +46,4 @@ const CalendarYear: React.FC<{ yearId: YearID }> = ({ yearId }) => {
7146
);
7247
};
7348

74-
export default memo(CalendarYear);
49+
export default memo(CalendarYear, (prev, next) => prev.model.id === next.model.id);

apps/core/src/features/calendar/CalendarYearScroll/CalendarYearScroll.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CalendarYear as CalendarYearModel } from '@repo/models';
12
import { VirtualListVertical } from '@repo/ui';
23
import { yearID } from '@repo/utils/calendar';
34
import React, { CSSProperties, memo, useCallback, useMemo } from 'react';
@@ -16,17 +17,35 @@ const CalendarYearScroll: React.FC = () => {
1617

1718
const startYear = useMemo(() => Temporal.Now.plainDateTimeISO().year, []);
1819

20+
const yearModelCache = useMemo(() => new Map<number, CalendarYearModel>(), []);
21+
1922
const getYearFromIndex = useCallback((index: number) => startYear + (index - MIDDLE_INDEX), [startYear]);
2023

24+
const getYearModel = useCallback(
25+
(year: number) => {
26+
if (!yearModelCache.has(year)) {
27+
const model = new CalendarYearModel(yearID(year));
28+
29+
yearModelCache.set(year, model);
30+
}
31+
32+
return yearModelCache.get(year)!;
33+
},
34+
[yearModelCache],
35+
);
36+
2137
const renderItem = useCallback(
2238
(index: number, style: CSSProperties) => {
39+
const year = getYearFromIndex(index);
40+
const model = getYearModel(year);
41+
2342
return (
2443
<div style={style} data-index={index}>
25-
<CalendarYear yearId={yearID(getYearFromIndex(index))} />
44+
<CalendarYear model={model} />
2645
</div>
2746
);
2847
},
29-
[getYearFromIndex],
48+
[getYearFromIndex, getYearModel],
3049
);
3150

3251
return (
@@ -36,6 +55,7 @@ const CalendarYearScroll: React.FC = () => {
3655
itemCount={TOTAL_YEARS}
3756
itemHeight={containerHeight}
3857
height={containerHeight}
58+
overscan={3}
3959
initialScrollOffset={MIDDLE_INDEX * containerHeight}
4060
classNameInner={s.root__inner}
4161
renderItem={renderItem}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.root {
22
position: absolute;
3+
contain: content;
4+
will-change: transform;
35
}

packages/ui/src/components/VirtualListVertical/VirtualListVertical.tsx

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import clsx from 'clsx';
2-
import { memo, CSSProperties, ReactNode } from 'react';
2+
import { memo, CSSProperties, ReactNode, useCallback, useRef, useState, useEffect } from 'react';
33

44
import { VirtualListItem } from './VirtualListItem';
5-
import { useVirtualList } from './hooks/useVirtualList';
65

76
import s from './VirtualListVertical.module.scss';
87

@@ -13,7 +12,7 @@ type VirtualListVerticalProps = {
1312
itemHeight: number;
1413
height: number;
1514
width?: string;
16-
overscan?: number;
15+
overscan: number;
1716
renderItem: (index: number, style: CSSProperties) => ReactNode;
1817
initialScrollOffset?: number;
1918
};
@@ -25,17 +24,39 @@ const VirtualListVertical: React.FC<VirtualListVerticalProps> = ({
2524
itemHeight,
2625
height,
2726
width = '100%',
28-
overscan = 2,
27+
overscan,
2928
renderItem,
3029
initialScrollOffset = 0,
3130
}) => {
32-
const { containerRef, onScroll, startIndex, endIndex, totalHeight } = useVirtualList({
33-
itemCount,
34-
itemHeight,
35-
height,
36-
overscan,
37-
initialScrollOffset,
38-
});
31+
const containerRef = useRef<HTMLDivElement>(null);
32+
const [scrollTop, setScrollTop] = useState(initialScrollOffset);
33+
const ticking = useRef(false);
34+
35+
const totalHeight = itemCount * itemHeight;
36+
37+
const handleScroll = useCallback(() => {
38+
if (!containerRef.current) {
39+
return;
40+
}
41+
const scroll = containerRef.current.scrollTop;
42+
43+
if (!ticking.current) {
44+
requestAnimationFrame(() => {
45+
setScrollTop(scroll);
46+
ticking.current = false;
47+
});
48+
ticking.current = true;
49+
}
50+
}, []);
51+
52+
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
53+
const endIndex = Math.min(itemCount - 1, Math.ceil((scrollTop + height) / itemHeight) + overscan);
54+
55+
useEffect(() => {
56+
if (containerRef.current && initialScrollOffset > 0) {
57+
containerRef.current.scrollTop = initialScrollOffset;
58+
}
59+
}, [initialScrollOffset]);
3960

4061
const items = [];
4162

@@ -54,22 +75,11 @@ const VirtualListVertical: React.FC<VirtualListVerticalProps> = ({
5475
return (
5576
<div
5677
ref={containerRef}
57-
onScroll={onScroll}
78+
onScroll={handleScroll}
5879
className={clsx(s.root, className)}
59-
style={{
60-
height,
61-
width,
62-
position: 'relative',
63-
overflow: 'auto',
64-
}}
80+
style={{ height, width, position: 'relative', overflow: 'auto' }}
6581
>
66-
<div
67-
style={{
68-
height: totalHeight,
69-
position: 'relative',
70-
}}
71-
className={clsx(s.root__inner, classNameInner)}
72-
>
82+
<div style={{ height: totalHeight, position: 'relative' }} className={clsx(s.root__inner, classNameInner)}>
7383
{items}
7484
</div>
7585
</div>

packages/ui/src/components/VirtualListVertical/hooks/useVirtualList.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,34 @@ type UseVirtualListParams = {
44
itemCount: number;
55
itemHeight: number;
66
height: number;
7-
overscan?: number;
7+
overscan: number;
88
initialScrollOffset?: number;
99
};
1010

1111
export function useVirtualList({
1212
itemCount,
1313
itemHeight,
1414
height,
15-
overscan = 5,
15+
overscan,
1616
initialScrollOffset = 0,
1717
}: UseVirtualListParams) {
1818
const containerRef = useRef<HTMLDivElement>(null);
1919
const [scrollTop, setScrollTop] = useState(initialScrollOffset);
2020
const visibleRangeRef = useRef({ start: 0, end: 0 });
21+
const tickingRef = useRef(false);
2122

2223
const totalHeight = itemCount * itemHeight;
2324

2425
const onScroll = useCallback((e: UIEvent<HTMLDivElement>) => {
25-
setScrollTop(e.currentTarget.scrollTop);
26+
const scroll = e.currentTarget.scrollTop;
27+
28+
if (!tickingRef.current) {
29+
window.requestAnimationFrame(() => {
30+
setScrollTop(scroll);
31+
tickingRef.current = false;
32+
});
33+
tickingRef.current = true;
34+
}
2635
}, []);
2736

2837
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);

0 commit comments

Comments
 (0)