【React】滚动性能优化——惰性加载的几种方式

前言

  • 惰性加载是一种优化手段,也可以看作是懒加载。
  • 这段时间我在写个项目,然后遇到滚动性能优化的问题,还踩了些坑,特此记录下。

需求

  • 有个长列表,数据是后端发来渲染出来的,这个列表如果特别长,要么做分页,要么做无限滚动。无限滚动下,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。(这句阮一峰博客原文)。

  • 我这次踩坑总结了3种方法:

第一种方法,蛮力解决

  • 我这人一开始死脑筋,直接想的就是蛮力解决。
  • 方法就是监听scroll,然后计算各种高度,最后算出在视口的元素是哪几个。进行渲染。不在视口的由div填充高度。
  • 代码:
function HomeProduct(props:Props){
    const [_,forceUpdate]=useState(0)
    useEffect(()=>{
        props.getProducts() ;
        props.homeContainer.current.addEventListener('scroll',throttle(()=>forceUpdate(x=>x+1),100))
    },[])
    let start =0
    let rootFontSize = parseFloat(document.documentElement.style.fontSize)
    if(props.homeContainer.current){
        let scrollTop = props.homeContainer.current.scrollTop
        let remainHeight = (scrollTop-(6)*rootFontSize)
        start =Math.floor(remainHeight/(4.2*rootFontSize)*2)
        if(start%2==1)start=start-1;
        start=start-2//倒退时前2个
    }
    return (
        <>
            <div className='home-product-list' key='list' >
                <h2  key='home-product-list-h2'>{props.currentCategory=='all'?'全部商品':props.currentCategory}</h2>
            
            {
                props.renderProduct.productList.map((item:Product,index:number)=>(
                    (index>=start && index<=start+9)?(
                        <Link key={item.id} to={{pathname:`/productdetail/${item.id}`,state:item}}
                        >
                        <Card
                            hoverable
                            key={item.id}
                            cover={<img src={item.poster}/>}
                        ><Card.Meta title={item.title} description={`价格:${item.price}元`} />
                        </Card>
                        </Link>

                    ):<div style={{height:`${4.5598*rootFontSize}px`,width:'42%'}} key = {item.id}></div>          
                ))
            }    
            </div>
       </>
    )
}
  • 这个homeContainer拿的是整个滚动区域的ref,监听它的滚动条。同时需要搞个forceUpdate刷新页面,不然当用户滚动时,页面填充的div就永远填充在那了。
  • scrollTop就不用说了,remainHeight就是卷曲的高度减去头部不属于列表项的高度。
  • rootFontSize就是根元素大小,都是用rem单位,所以需要乘它。
  • start就是起始索引。因为我这个页面做的是每行有2个,所以分母要乘2,并且还要看奇偶,奇数维持不动。
  • 另外上翻的时候需要看见前面2个元素,所以还要减2。
  • 最后在渲染的时候进行判断就行了。
  • 也可以把start放进useState里,react比较灵活,怎么搞都能弄出来。
  • 这种方法有个bug,就是你监听滚动条来渲染元素,元素拿div填充结果又会影响滚动条滚动,最终导致无限循环。虽然使用节流后,bug不是特别明显,但是在某些增加或者改变div的地方就会来回跳。就算div的高度设置的和原来元素一模一样,生成时也还会有跳动存在。

第二种方法,使用插件

  • 插件的话一般用的最多的是react-lazyload。用法很简单,就是拿这标签把图片包起来。
  • 但是我试了发现图片视口监测不正确。。。。可视区域内,图片有的出来有的没出来。
  • 我后面也没仔细研究了,因为我找到第三种方法。

第三种方法,使用IntercectionObserver

  • 我一开始走入误区了,觉得要把整个div给替换掉,结果感觉好像还增加了不少性能开销。实际把图片换掉就行。
  • 所以第一种方法,也许把图片换了就不会回跳。但监听scroll不是什么好选择,这里就用IntercectionObserver解决。
  • 这玩意用法不难,就是react使用时取dom和生成观察者会绊人。
  • 代码:
function HomeProduct(props:Props){
    const [io,setIo]=useState<IntersectionObserver>()
    const opts = {
        root:props.homeContainer.current
    }
    const callback=(e:IntersectionObserverEntry[])=>{
        e.forEach((item:IntersectionObserverEntry)=>{
            let ele = item.target as HTMLImageElement
            if(item.intersectionRatio===0){//不可见
                ele.src=''
            }else{
                ele.src=ele.dataset.src!
            }
        })
    }
    useEffect(()=>{
        props.getProducts()
        setIo(new IntersectionObserver(callback,opts))
    },[])
    return (
        <>
            <div className='home-product-list' key='list' >
                <h2  key='home-product-list-h2'>{props.currentCategory=='all'?'全部商品':props.currentCategory}</h2>
            {
                props.renderProduct.productList.map((item:Product,index:number)=>{
                  return (   
                        <Link key={item.id} to={{pathname:`/productdetail/${item.id}`,state:item}}
                        >
                        <Card
                            hoverable
                            key={item.id}
                            cover={      
                                <img 
                                data-src={item.poster}
                                ref={(ref)=>{
                                    if(ref){
                                        io!.observe(ref)
                                    }
                                }}></img>                
                            }
                        ><Card.Meta title={item.title} description={`价格:${item.price}元`}/>
                        </Card>
                        </Link>
                    )  
                  })
            }
            </div>
       </>
    )
}
  • 我使用useState把IntersectionObserver的实例放进去,在图片的地方使用ref可以批量获取所有图片的dom,然后调用实例方法对其观察。
  • 图片一开始不放src,先把链接存到data-src里。
  • 图片在视口进入会触发一次callback,退出也会触发一次callback。
  • opts的root就是我滚动的那个容器。
  • 拿到的e是IntersectionObserverEntry的数组,我这里用的typescript,传过来是啥类型都比较清楚。
  • 当intersectionRatio是0时表示元素退出容器,变为不可见状态。
  • 反过来就是可见状态,可见状态把src给赋值就行了。
  • 换图片地址有点水,可以拿个占位的替代下,但是思路一样的,占位的里面有个地方可以取到真实请求地址。

以前我没用框架写过一篇图片懒加载,文章地址。有需要代码可以拿去复制粘贴。

发布了163 篇原创文章 · 获赞 9 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/104688022