..
顺序渲染组件——客户端渲染场景下减少 CLS 的一个良方
背景
我们的网站采用了不同的技术实现方案,目前包括 Vue、React 和 Wordpress。为了保持 Header 与 Footer 的统一,这两个 UI 组件是使用 Web Component 实现的。
最近,我们用 React 重构了一个原本由 Flask Jinja 渲染的博客页面。上线后发现,这个页面的 CLS 指标上升得非常明显。
分析
通过 Chrome DevTools 分析后,我们发现了问题所在。
页面一开始就渲染了 Header 与 Footer。这两个组件与 React Root 是兄弟组件,并不由 React 管理。此外,这两个组件非常轻量,而 React 相对较重,并且还涉及 API 数据加载。
核心问题在于,页面最终效果不是顺序渲染的。Footer 本应出现在页面底部,但却与 Header 同时渲染出来。随后,在 React 加载和渲染的过程中,中间插入了新的元素,导致 Footer 的偏移量过大,造成明显抖动。
解决方案
解决方案其实很简单,就是让页面实现顺序渲染。但具体如何实现,就各显神通了。
针对我们遇到的问题,需要处理两个部分:
- 尽量保证 React 负责的部分是顺序渲染的。如果存在并行异步渲染的部分,最好提前设定好尺寸,或者在数据加载时并发进行,等到所有数据获取完成后再统一渲染,避免因先后渲染导致页面抖动。
- 在中间的 React 部分渲染完成之后,再渲染 Footer。
关键在于第二点:什么时间点才算是 React 页面渲染完毕?React 本身似乎没有提供渲染树完成的事件。
我最终想到的一个方案是:在页面生命周期中,当遇到第一个 API 请求静默超过 150ms 的时间点,即视为 React 页面渲染完毕。此时,触发 Footer 挂载。
以下是大致的 React Hook 代码:
/**
* 在 API 请求“波动平息”后渲染 footer,以减少累积布局偏移(CLS)。
*
* 参考:https://web.dev/articles/cls
*/
import { useEffect } from 'react';
import { apiWaveWatcher } from '../apis';
export const useFooter = () => {
useEffect(() => {
// apiWaveWatcher 会在 API 请求静默 150ms 后触发 onCalm
const unsub = apiWaveWatcher.onCalm(() => {
if (!document.querySelector('#gl-footer')) {
const footerEl = document.createElement('gl-footer');
footerEl.id = 'gl-footer';
document.body.appendChild(footerEl);
}
});
return () => {
unsub();
const footerEl = document.querySelector('#gl-footer');
if (footerEl) {
document.body.removeChild(footerEl);
}
};
}, []);
};