Appearance
首页白屏问题
在前端优化中,首页白屏是一个常见问题,当然造成白屏的原因有很多,实际开发中需要根据不同情况,灵活的处理。
问题
今天先暂时说其中一种场景:首屏加载过多重组件
vue
<script setup lang="ts">
import HeavyComp from './HeavyComp.vue';
</script>
<template>
<div class="container">
<template v-for="n in 100" :key="n">
// 组件内部循环创建 10000 个 dom 元素
<HeavyComp />
</template>
</div>
</template>
<style scoped>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1em;
}
</style>这段代码造成的问题是:第一次进入页面时,页面会停留在空白页面一段时间,最后等浏览器绘制完成后,一次性展示效果。
效果:
通过录制工具,可以看到渲染花费的时间:

js的执行时间为950ms,而渲染花费的时间用了2629ms,也就是用户需要在首屏等待接近3秒的时间,体验上会很差。
解决思路
既然同时渲染所有组件会给浏览器造成压力,那就让组件一个一个的渲染,分摊压力。先让用户看到一部分,然后再持续的渲染剩余的部分。
这样做其实渲染时间并不会缩短,甚至有可能会增加,但是对于用户的感知来说,从打开页面,到看到东西,这段时间会大幅缩减。
这里就要说到渲染帧: 就是在一个渲染时间线里面,它是分成了很多的小的时间段,大约每16.6ms一次。这样我们就可以利用帧数,逐一去渲染组件。
这时就可以利用window.requestAnimationFrame()
TIP
window.requestAnimationFrame()方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。
对回调函数的调用频率通常与显示器的刷新率相匹配。虽然 75hz、120hz 和 144hz 也被广泛使用,但是最常见的刷新率还是 60hz(每秒 60 个周期/帧)。为了提高性能和电池寿命,大多数浏览器都会暂停在后台选项卡或者隐藏的<iframe>中运行的requestAnimationFrame()。
我们可以给HeavyComp组件加上一个v-if="defer(n)"去判断,当n大于或等于了当前帧数时,defer函数才会返回true;
vue
<template>
<div class="container">
<template v-for="n in 100" :key="n">
<HeavyComp v-if="defer(n)" />
</template>
</div>
</template>完整代码
vue
<script setup lang="ts">
import { useDefer } from '@/hooks/useDefer';
import HeavyComp from './HeavyComp.vue';
const defer = useDefer();
</script>
<template>
<div class="container">
<template v-for="n in 100" :key="n">
<HeavyComp v-if="defer(n)" />
</template>
</div>
</template>
<style scoped>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1em;
}
</style>useDefer 的实现
ts
import { onUnmounted, ref } from 'vue';
type FnResult = (n: number) => boolean;
export function useDefer(maxCount = 100): FnResult {
const frameCount = ref(0);
let rafId: number;
function updateFrameCount() {
rafId = requestAnimationFrame(() => {
frameCount.value++;
if (frameCount.value >= maxCount) return;
updateFrameCount();
});
}
updateFrameCount();
onUnmounted(() => {
cancelAnimationFrame(rafId);
});
return function defer(n: number) {
return frameCount.value >= n;
};
}首先声明一个计数变量:frameCount,当每一次requestAnimationFrame执行时,frameCount就+1,表示当前渲染的帧数,增加了一帧。只要帧数没有达到最大帧数限制,就持续的调用updateFrameCount方法,不断的增加帧数。最后返回的defer函数,需要一个参数n, 参数n的作用就是用于判断当前帧是否大于了传入的n,如果大于就返回true让组件进行渲染。
最终效果:
可以看到,点击回车进入页面后,页面立马做出了响应,并且组件也是依次渲染,这样,就大幅增加了用户体验。