Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useVirtualList 配合 IntersectionObserver 出现抖动 #2324

Open
LaamGinghong opened this issue Sep 13, 2023 · 9 comments
Open

useVirtualList 配合 IntersectionObserver 出现抖动 #2324

LaamGinghong opened this issue Sep 13, 2023 · 9 comments

Comments

@LaamGinghong
Copy link

LaamGinghong commented Sep 13, 2023

最小复现 case:https://stackblitz.com/edit/stackblitz-starters-2ayxep?file=src%2FEllipsis.tsx

在超出默认视口范围后继续滚动会出现抖动的情况,因为内部用到了 IntersectionObserver 重新绘制视图,初步判断是出现重复计算引发的抖动

@liuyib
Copy link
Collaborator

liuyib commented Sep 13, 2023

我怀疑是掉帧,在你代码 IntersectionObserver 的 callback 里随便打印个 log,鼠标快速滚动一下可以看到很多输出,重排太多次了。我没记错的话 getComputedStyle, offsetHeight 都会触发重排。把 Ellipsis 的 逻辑都去掉,只放 JSX 结构,不会抖动。

首先,Ellipsis 里的逻辑肯定要做性能优化的。其次至于 useVirtualList 有多少性能问题在里面,目前我不确定。后面我找其他成熟的虚拟滚动库和 useVirtualList 对比下,写一些稍微复杂的场景,不只是纯文本展示。

综上,目前我能看到的抖动貌似是掉帧,不是 bug 类的抽搐。

@LaamGinghong LaamGinghong changed the title useVirtualList 配合 arco-design 的 Tooltip 抖动 useVirtualList 配合 IntersectionObserver 出现抖动 Sep 14, 2023
@LaamGinghong
Copy link
Author

@liuyib 有啥优化的建议吗?

@liuyib
Copy link
Collaborator

liuyib commented Sep 14, 2023

@liuyib 有啥优化的建议吗?

目前看,你的 demo 中每一行都是定高,高度就没必要重新算了(const { lineHeight } = window.getComputedStyle(ref.current, null) 相关逻辑可以省略),如果你后面要实现每一行不定高,那估计没法优化了。

另外,这个 demo 里的逻辑有几处比较奇怪的地方:

  1. Tooltip 没必要更新它的 disabled(不更新它的 disabled 就能用,为啥要大量计算去更新,为了测试 useVirtualList 性能嘛?)
  2. setVisible 设置的值始终是 true,看不出更新的意义(打印 lineHeightNum 是个远大于行高的值,所以 offsetHeight > (Number.isNaN(lineHeightNum) ? 32 : lineHeightNum) * 1; 就始终是 true

综上,目前 demo 里代码实现上还有些问题,就不做优化建议了。

@liuyib
Copy link
Collaborator

liuyib commented Sep 14, 2023

基于目前反馈的信息,没有发现 bug 类的异常哈,issue 就关了~

@liuyib liuyib closed this as completed Sep 14, 2023
@LaamGinghong
Copy link
Author

@liuyib 似乎不是,还是有点问题,我把订阅去掉了依然会有抖动,我猜测是因为使用的原生 scroll 导致的?因为我使用了另一个虚拟滚动的 hook,它用的是 react 的 onScroll 没有这个问题

@LaamGinghong
Copy link
Author

我比较好奇的是为什么不使用react 的 onScroll

@liuyib
Copy link
Collaborator

liuyib commented Sep 20, 2023

我使用了另一个虚拟滚动的 hook

发一下这个的 GitHub 地址呢,我去瞅下~ 也许可以参考优化下 ahooks 的 useVirtualList

@LaamGinghong
Copy link
Author

LaamGinghong commented Sep 21, 2023

公司内部的一个库,不过可以给你贴源码

import { useSize } from 'ahooks'
import { useEffect, useState, useMemo, UIEvent, useRef } from 'react'

export interface OptionType<T> {
  itemHeight: number | ((data: T, index: number) => number)
  overscan?: number
  onScroll?: (e: UIEvent<HTMLElement>) => void
}

export const useVirtualList = <T>(
  originalList: T[],
  options: OptionType<T>,
  deps: any[] = [],
) => {
  const { itemHeight, overscan = 5, onScroll } = options
  const scrollerRef = useRef<HTMLDivElement>(null)
  const size = useSize(scrollerRef)
  const [range, setRange] = useState({ start: 0, end: 10 })

  useEffect(() => {
    getListRange()
  }, [size?.width, size?.height, originalList.length])

  const totalHeight = useMemo(() => {
    if (typeof itemHeight === 'number') {
      return originalList.length * itemHeight
    }
    return originalList.reduce(
      (sum, data, index) => sum + itemHeight(data, index),
      0,
    )
  }, [originalList.length, ...deps])

  const list = useMemo(
    () =>
      originalList.slice(range.start, range.end).map((ele, index) => ({
        data: ele,
        index: index + range.start,
      })),
    [originalList, range],
  )

  const getListRange = () => {
    const element = scrollerRef.current
    if (element) {
      const offset = getRangeOffset(element.scrollTop)
      const viewCapacity = getViewCapacity(element.clientHeight)

      const from = offset - overscan
      const to = offset + viewCapacity + overscan
      setRange({
        start: from < 0 ? 0 : from,
        end: to > originalList.length ? originalList.length : to,
      })
    }
  }

  const getViewCapacity = (scrollerHeight: number) => {
    if (typeof itemHeight === 'number') {
      return Math.ceil(scrollerHeight / itemHeight)
    }
    const { start = 0 } = range
    let sum = 0
    let capacity = 0
    for (let i = start; i < originalList.length; i++) {
      const height = (itemHeight as (data: T, index: number) => number)(
        originalList[i],
        i,
      )
      sum += height
      capacity = i
      if (sum >= scrollerHeight) {
        break
      }
    }
    return capacity - start
  }

  const getRangeOffset = (scrollTop: number) => {
    if (typeof itemHeight === 'number') {
      return Math.floor(scrollTop / itemHeight) + 1
    }
    let sum = 0
    let offset = 0
    for (let i = 0; i < originalList.length; i++) {
      const height = (itemHeight as (data: T, index: number) => number)(
        originalList[i],
        i,
      )
      sum += height
      if (sum >= scrollTop) {
        offset = i
        break
      }
    }
    return offset + 1
  }

  const getDistanceTop = (index: number) => {
    if (typeof itemHeight === 'number') {
      const height = index * itemHeight
      return height
    }
    const height = originalList
      .slice(0, index)
      .reduce((sum, data, index) => sum + itemHeight(data, index), 0)
    return height
  }

  const scrollTo = (index: number) => {
    if (scrollerRef.current) {
      scrollerRef.current.scrollTop = getDistanceTop(index)
      getListRange()
    }
  }

  return {
    list,
    scrollTo,
    scrollerRef,
    scrollerProps: {
      ref: scrollerRef,
      onScroll: (e: UIEvent<HTMLElement>) => {
        e.preventDefault()
        getListRange()
        if (onScroll) {
          onScroll(e)
        }
      },
      style: { overflowY: 'auto' } as { overflowY: 'auto' },
    },
    wrapperProps: {
      style: {
        width: '100%',
        height: totalHeight,
        paddingTop: getDistanceTop(range.start),
      },
    },
  }
}

@liuyib liuyib reopened this Sep 21, 2023
@LaamGinghong
Copy link
Author

我使用了另一个虚拟滚动的 hook

发一下这个的 GitHub 地址呢,我去瞅下~ 也许可以参考优化下 ahooks 的 useVirtualList

它们也是 fork 你们实现😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants