前端体验优化。初尝试React 顺序加载组件

572 阅读3分钟

前言

前端小白,仅提供思路。

背景

由于页面接口返回的影响,导致一部分组件无法渲染,优先渲染不需要接口数据的组件。当接口数据返回时,会导致页面突然抖动,给用户造成不好体验。

CLS(cumulative layout shift) 累计偏移量

用于量化页面抖动的指标 ( web.dev/cls/ )。可用CHROME拓展Web Vitals查看当前页面CLS。

常见优化CLS方案:

  1. 使用占位符
    优点:用户看到的内容及所有内容。
    缺点:接口挂掉或根据数据内容不展示此组件依旧会造成页面抖动。
  2. 组件顺序加载展示
    优点:组件根据逻辑顺序加载,不会造成抖动。
    缺点:头部组件没加载出时,底部组件不会展示。
  3. 动画过度(不会降低,用户体验更好)
    优点:动画过度比较丝滑
    缺点:并不会降低CLS值
  4. 服务端渲染 以下GIF依次为直接展示,占位符,顺序加载和动画效果
    直接展示
占位符 顺序加载 动画效果

本文实现的顺序加载组件RenderOrder

使用方式

RenderOrder组件将需要顺序加载的组件包裹,每个Cmp都需要load属性。
Cmp2的load属性为true时或者已经到达两秒,才会渲染Cmp3

<RenderOrder>
    <Cmp1 load />
    <Cmp2 load = {false} timeout = {2}/> 
    <Cmp3 load /> 
</RenderOrder >

大体实现思路

需要一个RenderOrder类

class RenderOrder {
    timeout: number;      // renderOrder组件的超时时间 
    renderIndex = 0;      // 渲染到第几个Child
    force = false;        // 是否进行强制渲染
    childIsArray = false; // RenderOrder组件内部是否只有一个Child
    renderFlagList = []   // 
    timeoutItemList = []; // child超时列表
    renderItem = [];      // 需要渲染的child列表
}

组件创建时,需要对组件设置的超时时间进行倒计时。如已经到达倒计时时间且child的load属性为false,这强制进行渲染。

计时分为两部分

  1. RenderOrder超时,所有child都会渲染。
  2. child超时,将此child的load属性设置为true。
 setTimeout(() => {
    this.force = true;
    this.forceUpdate();
    }, this.timeout * 1000);

// item超时
(children as Array<ReactElement>).forEach((v: ReactElement, i: number) => {
    const { timeout = 10, load = true } = v.props;
    this.renderFlagList.push(load);
    setTimeout(() => {
        this.timeoutItemList.push(i);
    }, timeout * 1000);
 });

由于达到超时时间的child不会触发更新,需设置轮询来判断当前是否有child已到超时时间。如有,则对组件进行强制渲染。
这里使用requestAnimationFrame进行轮询。
以下为大致方法:

 rafTimeout = () => {
    let needForceUpdate = false;

    requestAnimationFrame(() => {
            if (this.timeoutItemList.length && !this.force) {
                this.timeoutItemList.forEach(v => {
                    const { children } = this.props;
                    // 阻止load已经变为true的Item导致强制渲染
                    const { load = true } = (children as Array<ReactElement>)[v].props;
                    if (load !== true) {
                        this.renderFlagList[v] = true;
                        needForceUpdate = true;
                    }
                });
                if (needForceUpdate) {
                    this.forceUpdate();
                }
                this.timeoutItemList = [];
            }

            if (!this.force) {
                this.rafTimeout();
            }
        });
    };

最后就是组件的render
每次render前需要获取那些child可以进行渲染
获取方法大致如下

getRenderList = () => {
    const { children = [] } = this.props;
    for (let i = 0; i < (children as Array<ReactElement>).length; i++) {
        const node = (children as Array<ReactElement>)[i];
        const flag = this.renderFlagList[i];
        const { load = true, callBack } = node.props;

        // 遇到未完成直接退出
        if (!this.force && !(load || flag)) {
            break;
         }

        // 完成的对象,执行callback。使用index判断,防止callBack执行两次
        if (callBack && i > this.renderIndex) {
            callBack();
        }
        
        if (this.renderIndex < (children as Array<ReactElement>).length) {
            this.renderIndex += 1;
        }
     }
    this.renderItem = this.renderItem.map((v: ReactElement, index: number) => {
        return { ...v, props: { ...v.props, show: index < this.renderIndex } };
    });
};

子组件处理

使用一个组件包裹了子组件,并赋予其动画效果,让其展示更丝滑

export function Item(props: IItemProps): ReactElement {
    const { show, fadeIn, needHeightAni, heightAni, children } = props;
    let cls = cs({
        // 更改组件可见属性,使用visibility,方便照片并发请求
        'render-order-item-hidden': !show,
        // 缓入动画
        'render-order-item-fadein': fadeIn,
        // 高度变化动画
        'render-order-item-needHeightAni': needHeightAni,
        'render-order-item-heightAni': heightAni,
    });
    return <div className={cls}>{children}</div>;
}

总结

原理很简单,就是判断要渲染的组件的上一组件是否已经染完渲染。完成,则渲染;未完成,则等待其完成。