在我们开发过程中,可能会出现一个页面渲染多个大组件的情况,这里所谓的大组件是指这个组件包含了很多的Dom元素,所以因为一次性渲染过多的dom,导致页面出现白屏的问题。
这里我模拟一个比较大的组件,然后再App组件里面一次性渲染了多个
javascript
const HeavyComponent = () => { return <div> hello { new Array(5000).fill(1).map((item, index) => { return <div className='item'> index; </div> }) } </div>}function App() { const [count, setCount] = useState(0); const defer = useDelay(); return <div className='' > { new Array(100).fill(0).map((item, index) => { // return defer(index) && <HeavyComponent/> return <HeavyComponent/> }) } </div>}
这里会明显感觉到首页白屏的情况
1.回顾一下浏览器解析HMTL的过程
当浏览器的网络线程收到 HMLT 文档后,会生成一个渲染任务,并将其传递给渲染主线程的消息队列。
在时间循环机制的作用下,渲染主线程会冲消息队列中取出一个渲染任务,开始渲染流程
渲染流程分为多个阶段:
- HMTL解析
- 样式计算
- 布局
- 分层
- 绘制
- 分块
- 光栅化
每个阶段都有明确的输入输入出,上一个阶段的输出变成下一个阶段的输入,形成一个渲染流水线
1.1 解析HTML
从上到下解析html文件,过程中遇到 css 解析 css ,遇到 JS 执行 JS,为了提高解析效率,浏览器在解析之前会预先去交由 与解析线程 下载外部的CSS 和 JS文件
如果主线程在遇到 link 外部CSS时,主线程不会等待,继续执行后面HTML解析,因为下载CSS和解析CSS都不是在主线程中执行的,解析完成生成 CSSOM 后再交给主线程,下载由网络线程完成,解析由负责解析的线程完成,所以这也是CSS不会阻塞HMTL解析的更本原因
如果主线程解析到 Script 标签,会停止HTML的解析,转而等待 JS 文件下载完成,并执行完成后在解析 HTML 。这是因为 JS 代码执行过程中可能会修改当前的DOM 树,所以 DOM 树的生成必须停止,这也是 JS 会阻塞 HTML 解析的根本原因。
第一步完成后,会得到 DOM 树 和 CSSOM 树。
1.2 样式计算
主线程会遍历得到的 DOM ,依照每个节点计算出最终的样式,称之为 样式计算 (Computed Style)
这一过程中会把很多预设值变成绝对值 比如 red 变成 rgb(255,0,0), 相对单位会变成绝对单位(em 、rem、vh、vW)变成 px
这一步完成后一颗带有样式的 树
基于CSSOM 和 DOM树生成 渲染树 ,这里需要主要 CSSOM 和 DOM 是会相互等待的,也就是说必须两个都完成后才开下下一步生成 渲染树,css不会影响 HTML的解析,但是会影响后续的绘制,但是基本不用担心,css 解析成 CSSOM 是非常快的
1.3 布局-- layout
布局阶段会一次遍历每一个DOM 树的节点,计算节点的几何形信息,例如 节点的宽高,在屏幕上的位子
1.4 分层
主线程会使用一套复杂的策略来对整个布局树做分层处理
主要是为了在某些情况下产生了重排,能使得影响到的地方最小化,从而提高效率。
滚动条、堆叠上下文、transform、等样式或多或少会影响分层的结果。
1.5 光栅化
光栅化其实就是对集合图形的 所覆盖的区域,进行像素点着色情况输出
主线程会为了每层单独产生绘制指令集,用于把这一程的内容绘制出来,完成绘制后主线程会讲每一个图层的绘制信息交个合成线程,剩余工作由合成线程完成。
合成线程先对每一个图层进行分块,将其划分为更多的小区域。
分块完成后合成线程就将分块信息交给GPU 进程,完成光栅化,最后绘制到页面上
2.如何使用延迟加载的方式解决
2.1 白屏原因
经过我们了解HTML的解析的过程了解到,JS的解析执行是会影响HTML解析的,加之我们一般的SPA 应用的dom 元素都是靠JS执行生成的,所以在需要一次性 js 生成太多的dom导致白屏。
2.2 解决办法
这是我在网上找的一张关于一帧 生命周期的图片
其中我们主要关注的就是 requestAnimationFiame 和 requestIdleCallback这两个API调用的时机,一个是在绘制之前调用,一个是在 一帧中有剩余时间是调用,当然具体的一些信息可以去文档看看,这里就不赘述了。
requestIdleCallback 的思想也是React fiber架构中 Schedule 调度模块优先级调度可中断更新的实现思路
本质上来说只要我们不让大量的DOM 在同一时间去渲染就好了,所以我们可以考虑分帧渲染的方法。但是考虑到兼容性相关的问题所以我这里使用了 requestAnimationFiame 来做延迟加载的功能具体完整代码如下
scss
const useDelay = (max = 0) => { const ref = useRef(0); let rafId: any; const updateFramecount = () => { rafId = requestAnimationFrame(()=>{ ref.current ++; if (ref.current > max){ return; } updateFramecount(); }) } useEffect(() => { return () => { if (rafId){ cancelAnimationFrame(rafId); } } }, []) return function defer(n:any){ // console.log(ref.current, n); return ref.current >= n; }}
const HeavyComponent = () => { return <div> hello { new Array(5000).fill(1).map((item, index) => { return <div className='item'> index; </div> }) } </div>}function App() { const [count, setCount] = useState(0); const defer = useDelay(); return <div className='' > { new Array(100).fill(0).map((item, index) => { return defer(index) && <HeavyComponent/> // return <HeavyComponent/> }) } </div>}
为什么不直接使用setTimeout 或者 setInterval呢
事件循环机制大家应该都有所了解,这里讲讲主要原因
我们时间循环机制中 分为 微任务队列和 宏任务队列,setTimeout 和 setInterval 是进入宏任务列表的,理论上如果我们的微任务列表中一直有任务,或者某个大任务执行时间很长,那么我们的宏任务队列里面就会很久都得不到执行。所以也有可能出现问题。
结尾
最后有兴趣的同学可以试一下,如果有什么问题欢迎大家给我提出宝贵的意见和建议