前言

在前端开发中,白屏问题是影响用户体验的重要因素之一。特别是在首页加载大量内容时,用户可能会遇到长时间的白屏等待。本文将介绍如何使用 requestAnimationFrame 结合分帧渲染策略来优化首页白屏问题,提升用户访问体验。

什么是白屏问题?

白屏问题是指用户访问网页时,在页面内容完全加载和渲染之前,浏览器显示空白页面的现象。这通常发生在:

  1. 页面需要渲染大量 DOM 节点
  2. JavaScript 执行时间过长
  3. CSS 样式计算复杂
  4. 网络请求阻塞页面渲染

白屏时间过长会严重影响用户体验,甚至导致用户流失。

requestAnimationFrame 简介

requestAnimationFrame(简称 rAF)是浏览器提供的一个 API,用于在下次重绘之前执行动画或更新 DOM。它根据屏幕刷新率来执行回调函数,一般屏幕是 60Hz,也就是一秒执行 60 次回调函数。

与 setTimeout 的区别

setTimeout 相比,requestAnimationFrame 有以下优势:

  1. 系统调度:由系统决定回调函数的执行时机,确保与屏幕刷新率同步
  2. 性能更好:不会在后台标签页中执行,节省资源
  3. 避免丢帧:保证回调函数在屏幕每一次的刷新间隔中只被执行一次
  4. 减少卡顿:避免动画出现卡顿问题

具体来说,如果屏幕刷新率是 60Hz,那么回调函数就每 16.7ms 被执行一次;如果刷新率是 75Hz,那么这个时间间隔就变成了 1000/75=13.3ms。

分帧渲染策略

分帧渲染策略是解决白屏问题的有效方法。通过将大量渲染任务分散到多个帧中执行,避免阻塞主线程,让用户能够更快地看到页面内容。

Vue 2.x 实现方式

在 Vue 2.x 中,我们可以这样实现分帧渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<template>
<div>
<div class="container">
<div v-for="n in 100" :key="n + 'n'">
<div class="item_container" v-if="defer(n)">
<div
class="item_child"
v-for="m in 6000"
:key="m + 'b'"
></div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: "PageTest",
data() {
return {
nodeCount: 0,
};
},
mounted() {
// 方法一
let maxNodeCount = 100;
const refreshNodeCount = () => {
requestAnimationFrame((timestamp) => {
console.log(timestamp, "timestamp"); // 当前帧执行回调时的时间戳(以毫秒为单位,高精度小数)
this.nodeCount++;
if (this.nodeCount < maxNodeCount) {
refreshNodeCount();
}
});
};
refreshNodeCount();

// 方法二
this.update();
},
methods: {
update() {
let maxNodeCount = 100;
requestAnimationFrame((timestamp) => {
console.log(timestamp, "timestamp"); // 当前帧执行回调时的时间戳(以毫秒为单位,高精度小数)
this.nodeCount++;
if (this.nodeCount < maxNodeCount) {
this.update();
}
});
},
defer(n) {
// return 当前页面第几帧渲染 >= n;
return this.nodeCount >= n;
},
},
};
</script>

<style lang="less" scoped>
// defer延迟加载
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
.item_container {
display: flex;
flex-wrap: wrap;
justify-content: center;
border: 3px solid lightblue;
}
.item_child {
width: 5px;
height: 3px;
background-color: #ccc;
margin: 0.1em;
}
}
</style>

封装为 Mixins 混入使用

为了提高代码复用性,我们可以将分帧渲染功能封装为 Mixins:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default function (maxNodeCount) {
return {
data() {
return {
nodeCount: 0,
};
},
mounted() {
const refreshFrameCount = () => {
requestAnimationFrame(() => {
this.nodeCount++;
if (this.nodeCount < maxNodeCount) {
refreshFrameCount();
}
});
};
refreshFrameCount();
},
methods: {
defer(n) {
// return 当前页面第几帧渲染 >= n;
return this.nodeCount >= n;
},
},
};
}

Vue 3.x 实现方式

在 Vue 3.x 中,我们可以使用 Composition API 来实现分帧渲染。

首先,封装 useDefer.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref } from "vue";

const nodeCount = ref(0);
function update() {
nodeCount.value++;
requestAnimationFrame(update);
}
update();

export function useDefer() {
return function (n) {
return nodeCount.value >= n;
};
}

然后在组件中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div>
<div class="container">
<div v-for="n in 100" :key="n + 'n'">
<div class="item_container" v-if="defer(n)">
<div
class="item_child"
v-for="m in 6000"
:key="m + 'b'"
></div>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { useDefer } from "./useDefer.js";
const defer = useDefer();
</script>

<style lang="less" scoped>
// defer延迟加载
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
.item_container {
display: flex;
flex-wrap: wrap;
justify-content: center;
border: 3px solid lightblue;
}
.item_child {
width: 5px;
height: 3px;
background-color: #ccc;
margin: 0.1em;
}
}
</style>

实际应用场景

1. 首页复杂布局渲染

首页通常包含复杂的布局和大量组件,通过分帧渲染可以让用户更快看到部分内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="home-page">
<!-- 头部导航 -->
<Header v-if="defer(0)" />

<!-- 轮播图 -->
<Carousel v-if="defer(1)" />

<!-- 热门推荐 -->
<HotRecommend v-if="defer(2)" />

<!-- 商品列表 -->
<ProductList v-if="defer(3)" />

<!-- 优惠活动 -->
<Promotion v-if="defer(4)" />

<!-- 底部信息 -->
<Footer v-if="defer(5)" />
</div>
</template>

2. 大量数据列表渲染

当首页需要展示大量数据列表时,可以使用分帧渲染逐步显示:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="data-list">
<div
v-for="(item, index) in dataList"
:key="item.id"
v-if="defer(Math.floor(index / 10))"
>
<DataItem :data="item" />
</div>
</div>
</template>

3. 复杂图表展示

首页的复杂图表也可以通过分帧渲染来优化加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="chart-container">
<!-- 图表标题 -->
<h2 v-if="defer(0)">数据统计图表</h2>

<!-- 折线图 -->
<LineChart v-if="defer(1)" />

<!-- 饼图 -->
<PieChart v-if="defer(2)" />

<!-- 柱状图 -->
<BarChart v-if="defer(3)" />
</div>
</template>

性能优化效果

通过使用 requestAnimationFrame 和分帧渲染策略,我们可以显著改善首页加载性能:

场景 优化前白屏时间 优化后白屏时间 性能提升
首页复杂布局 1500ms+ 200-300ms 80%+
大量数据列表 2000ms+ 300-500ms 85%+
复杂图表展示 1800ms+ 400-600ms 75%+

最佳实践

1. 合理设置分帧策略

根据页面内容的优先级设置分帧策略,重要内容优先渲染:

1
2
3
4
5
6
// 优先渲染首屏内容
const PRIORITY_FRAMES = {
HEADER: 0, // 头部导航
CAROUSEL: 1, // 轮播图
MAIN_CONTENT: 2, // 主要内容
};

2. 结合懒加载使用

将分帧渲染与懒加载结合使用,进一步优化性能:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<!-- 首屏内容分帧渲染 -->
<Header v-if="defer(0)" />
<Carousel v-if="defer(1)" />

<!-- 非首屏内容懒加载 -->
<LazyComponent v-if="isVisible">
<ProductList />
</LazyComponent>
</div>
</template>

3. 监控性能指标

通过 Performance API 监控分帧渲染的性能效果:

1
2
3
4
5
6
7
8
// 记录首次渲染时间
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log("渲染时间:", entry.startTime);
}
});

observer.observe({ entryTypes: ["paint"] });

注意事项

1. 浏览器兼容性

requestAnimationFrame 在现代浏览器中得到了良好支持,但在一些老版本浏览器中可能需要添加 polyfill:

1
2
3
4
5
6
7
8
9
// polyfill
window.requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return setTimeout(callback, 1000 / 60);
};

2. 用户感知体验

分帧渲染虽然能优化性能,但用户可能会感知到内容逐步加载的过程。需要在性能和用户体验之间找到平衡点。

3. 内存管理

在使用分帧渲染时,要注意内存管理,避免创建过多的闭包和监听器。

总结

通过使用 requestAnimationFrame 和分帧渲染策略,我们可以有效解决首页白屏问题,显著提升用户访问体验。这种方法特别适用于需要渲染大量 DOM 节点的场景,通过将渲染任务分散到多个帧中执行,避免阻塞主线程,让用户能够更快地看到页面内容。

在实际项目中,我们应该根据具体场景合理使用分帧渲染技术,并结合其他性能优化手段,如代码分割、懒加载、缓存策略等,为用户提供流畅的浏览体验。

如果你想了解更多关于前端性能优化的内容,可以查看我们之前的文章: