feat: optimize highlight render performance

This commit is contained in:
EnixCoda 2022-06-05 02:46:20 +08:00
parent caaff93ad7
commit b0283f1f5e
4 changed files with 86 additions and 27 deletions

View file

@ -1,6 +1,9 @@
import * as React from 'react'
export function Highlight(props: { text: string; match?: RegExp | string }) {
export const Highlight = React.memo(function Highlight(props: {
text: string
match?: RegExp | string
}) {
const { text, match } = props
const $match = React.useMemo(() => {
if (match) {
@ -13,23 +16,32 @@ export function Highlight(props: { text: string; match?: RegExp | string }) {
return null
}, [match])
if (!$match) return <>{text}</>
const chunks = useChunks(text, $match)
return <>{chunks.map(([type, text], key) => React.createElement(type, { key }, text))}</>
})
function useChunks(text: string, $match: RegExp | null) {
const contents: [string, string][] = []
if ($match === null) return [['span', text]]
const matchedPieces = Array.from(text.matchAll($match)).map(
([text, highlightText = text]) => highlightText,
)
const preservedPieces = text.split($match)
const content = []
let i = 0
while (matchedPieces.length || preservedPieces.length) {
if (preservedPieces.length) {
content.push(<span key={i++}>{preservedPieces.shift()}</span>)
}
if (matchedPieces.length) {
content.push(<mark key={i++}>{matchedPieces.shift()}</mark>)
}
const push = (type: string, text: string) => {
if (!text) return
const last = contents[contents.length - 1]
if (last && last[0] === type) last[1] += text
else contents.push([type, text])
}
return <>{content}</>
let i = 0
while (i < Math.max(matchedPieces.length, preservedPieces.length)) {
push('span', preservedPieces[i])
push('mark', matchedPieces[i])
++i
}
return contents
}

View file

@ -0,0 +1,27 @@
import { render } from '@testing-library/react'
import React from 'react'
import { is } from 'utils/is'
import { HighlightOnIndexes } from './HighlightOnIndexes'
function test(title: string, text: string, indexes?: number[]) {
it(title, () => {
expect(render(<HighlightOnIndexes text={text} indexes={indexes} />).container.textContent).toBe(
text,
)
})
}
const text = 'abcdef'
for (let i = 0; i < Math.pow(2, text.length); i++) {
const bitwise = i.toString(2)
const indexes = bitwise
.split('')
.reverse()
.map((bit, index) => (bit === '1' ? index : false))
.filter(is.not.false)
test(
`renders properly when highlight ${bitwise.padStart(text.length, '0')}, indexes [${indexes}]`,
text,
indexes,
)
}

View file

@ -1,17 +1,28 @@
import * as React from 'react'
export function HighlightOnIndexes(props: { text: string; indexes?: number[] }) {
const { text, indexes } = props
if (!indexes?.length) return <>{text}</>
import * as React from 'react';
export function HighlightOnIndexes({ text, indexes = [] }: { text: string; indexes?: number[] }) {
return (
<>
{text
.split('')
.map((char, i) =>
indexes.includes(i) ? <mark key={i}>{char}</mark> : <span key={i}>{char}</span>,
)}
{[-1]
.concat(indexes)
.map((index, i, arr) => [
index === -1 ? '' : text.slice(index, index + 1),
text.slice(index + 1, arr[i + 1]),
])
.reduce((arr, pair) => {
const last = arr[arr.length - 1]
if (last && !last[1]) {
last[0] += pair[0]
last[1] += pair[1]
} else {
arr.push(pair)
}
return arr
}, [] as string[][])
.map(([chunk, nextChunk], i) => [
chunk && <mark key={i * 2}>{chunk}</mark>,
nextChunk && <span key={i * 2 + 1}>{nextChunk}</span>,
])}
</>
)
}

View file

@ -1,5 +1,6 @@
import { ReactElement } from 'react'
import * as ReactDOM from 'react-dom'
import { is } from './is'
export function pick<T>(source: T, keys: string[]): Partial<T> {
if (keys && typeof keys === 'object') {
@ -129,12 +130,20 @@ export async function JSONRequest<D>(
).json()
}
export function searchKeyToRegexp(searchKey: string) {
if (!searchKey) return null
return safeRegexp(searchKey, hasUpperCase(searchKey) ? 'g' : 'gi')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function memoize<Args extends any[], R>(fn: (...args: Args) => R): (...args: Args) => R {
let lastArgs: Args | null = null
let lastR: R | null = null
return (...args) => {
if (lastArgs && is.shallowEqual.array(lastArgs, args)) return lastR as R
return (lastR = fn(...(lastArgs = args)))
}
}
export const searchKeyToRegexp = memoize((searchKey: string) =>
searchKey ? safeRegexp(searchKey, hasUpperCase(searchKey) ? 'g' : 'gi') : null,
)
export function hasUpperCase(input: string) {
return /[A-Z]/.test(input)
}