前言
前端小白,仅提供思路。
背景
由于页面接口返回的影响,导致一部分组件无法渲染,优先渲染不需要接口数据的组件。当接口数据返回时,会导致页面突然抖动,给用户造成不好体验。
CLS(cumulative layout shift) 累计偏移量
用于量化页面抖动的指标 ( web.dev/cls/ )。可用CHROME拓展Web Vitals查看当前页面CLS。
常见优化CLS方案:
- 使用占位符
优点:用户看到的内容及所有内容。
缺点:接口挂掉或根据数据内容不展示此组件依旧会造成页面抖动。- 组件顺序加载展示
优点:组件根据逻辑顺序加载,不会造成抖动。
缺点:头部组件没加载出时,底部组件不会展示。- 动画过度(不会降低,用户体验更好)
优点:动画过度比较丝滑
缺点:并不会降低CLS值- 服务端渲染 以下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,这强制进行渲染。
计时分为两部分
- RenderOrder超时,所有child都会渲染。
- 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>;
}
总结
原理很简单,就是判断要渲染的组件的上一组件是否已经染完渲染。完成,则渲染;未完成,则等待其完成。