Skip to content

Commit b27211c

Browse files
committed
perf: optimize HighlightedText segmentation and rendering
1 parent bf13f84 commit b27211c

File tree

1 file changed

+68
-62
lines changed

1 file changed

+68
-62
lines changed

src/components/HighlightedText/index.tsx

Lines changed: 68 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
import React, {useMemo, useCallback} from 'react';
2-
import {
3-
Text,
4-
StyleSheet,
5-
type StyleProp,
6-
type TextStyle,
7-
TouchableWithoutFeedback,
8-
} from 'react-native';
1+
import React from 'react';
92
import type {
103
TextSegmentProps,
114
HighlightedTextProps,
125
HighlightedSegmentProps,
136
} from '../types';
7+
import {Text, StyleSheet} from 'react-native';
148

159
const HighlightedText: React.FC<HighlightedTextProps> = ({
1610
text,
@@ -20,85 +14,97 @@ const HighlightedText: React.FC<HighlightedTextProps> = ({
2014
onHighlightedPress,
2115
...rest
2216
}) => {
23-
const segments = useMemo(() => {
24-
const sorted = [...highlights].sort((a, b) => a.start - b.start);
17+
const baseStyle = [style, highlightedStyle ?? styles.isHighlighted];
18+
19+
const segments = React.useMemo(() => {
20+
if (!text || !highlights.length) {
21+
return [];
22+
}
23+
let cursor = 0;
24+
let isSorted = true;
2525

2626
const parts: TextSegmentProps[] = [];
27-
let currentIndex = 0;
2827

29-
for (let i = 0; i < sorted.length; ++i) {
30-
const currentSegment = sorted[i] as HighlightedSegmentProps;
31-
const {start, end} = currentSegment;
28+
for (let i = 1; i < highlights.length; i++) {
29+
if (highlights[i - 1]!.start > highlights[i]!.start) {
30+
isSorted = false;
31+
break;
32+
}
33+
}
34+
const ordered = isSorted
35+
? highlights
36+
: [...highlights].sort((a, b) => a.start - b.start);
37+
38+
for (let i = 0; i < ordered.length; i++) {
39+
const {
40+
end,
41+
start,
42+
style: segmentStyle,
43+
} = ordered[i] as HighlightedSegmentProps;
44+
45+
if (start >= text.length || end <= cursor) continue;
3246

33-
if (start > currentIndex) {
47+
const clampedStart = Math.max(cursor, start);
48+
const clampedEnd = Math.min(end, text.length);
49+
50+
if (clampedStart > cursor) {
3451
parts.push({
3552
isHighlighted: false,
36-
text: text.slice(currentIndex, start),
53+
text: text.slice(cursor, clampedStart),
3754
});
3855
}
3956
parts.push({
40-
...currentSegment,
57+
end: clampedEnd,
58+
start: clampedStart,
59+
style: segmentStyle,
4160
isHighlighted: true,
42-
text: text.slice(start, end),
61+
text: text.slice(clampedStart, clampedEnd),
4362
});
44-
currentIndex = end;
63+
64+
cursor = clampedEnd;
4565
}
46-
if (currentIndex < text.length) {
66+
if (cursor < text.length) {
4767
parts.push({
4868
isHighlighted: false,
49-
text: text.slice(currentIndex),
69+
text: text.slice(cursor),
5070
});
5171
}
5272
return parts;
5373
}, [highlights, text]);
5474

55-
const getHighlightedSegmentStyle = useCallback(
56-
(isHighlighted = false, segmentStyle?: StyleProp<TextStyle>) => {
57-
return !isHighlighted
58-
? style
59-
: StyleSheet.flatten([
60-
style,
61-
highlightedStyle ?? styles.isHighlighted,
62-
segmentStyle,
63-
]);
75+
const onHighlightedSegmentPress = React.useCallback(
76+
(segment: TextSegmentProps) => {
77+
if (!segment.isHighlighted) return;
78+
onHighlightedPress?.({
79+
end: segment.end!,
80+
start: segment.start!,
81+
text: segment.text,
82+
});
6483
},
65-
[highlightedStyle, style],
84+
[onHighlightedPress],
6685
);
6786

68-
const renderText = useCallback(
69-
(segment: TextSegmentProps, index: number) => {
70-
return !segment.isHighlighted ? (
71-
<Text
72-
key={index}
73-
style={getHighlightedSegmentStyle(segment.isHighlighted)}>
74-
{segment.text}
75-
</Text>
76-
) : (
77-
<TouchableWithoutFeedback
78-
key={index}
79-
onPress={() =>
80-
onHighlightedPress?.({
81-
end: segment.end!,
82-
text: segment.text,
83-
start: segment.start!,
84-
})
85-
}>
86-
<Text
87-
style={getHighlightedSegmentStyle(
88-
segment.isHighlighted,
89-
segment.style,
90-
)}>
91-
{segment.text}
92-
</Text>
93-
</TouchableWithoutFeedback>
94-
);
95-
},
96-
[getHighlightedSegmentStyle, onHighlightedPress],
97-
);
87+
const renderText = (segment: TextSegmentProps, index: number) => {
88+
const segmentStyle = segment.isHighlighted
89+
? segment.style
90+
? [baseStyle, segment.style]
91+
: baseStyle
92+
: style;
93+
94+
return (
95+
<Text
96+
style={segmentStyle}
97+
suppressHighlighting
98+
key={`segment-${index}`}
99+
onPress={() => onHighlightedSegmentPress(segment)}>
100+
{segment.text}
101+
</Text>
102+
);
103+
};
98104

99105
return (
100106
<Text style={style} {...rest}>
101-
{segments.map(renderText)}
107+
{segments.length ? segments.map(renderText) : text}
102108
</Text>
103109
);
104110
};

0 commit comments

Comments
 (0)