# Vue怎么防止白屏添加首屏动画
## 前言:理解白屏问题的本质
在单页应用(SPA)开发中,白屏问题是一个常见的性能优化痛点。当用户首次访问Vue应用时,需要经历以下过程:
1. 下载HTML骨架
2. 加载JavaScript文件
3. 执行Vue初始化
4. 渲染真实DOM
这个过程可能需要数百毫秒甚至数秒时间,在这期间用户看到的将是空白页面(即"白屏")。本文将深入探讨如何通过技术手段解决这个问题,并实现优雅的首屏动画体验。
## 一、白屏问题的根本原因分析
### 1.1 浏览器渲染机制
现代浏览器的渲染流程包括:
- HTML解析 → CSSOM构建 → 渲染树生成 → 布局 → 绘制
### 1.2 Vue应用的特殊性
与传统服务端渲染不同,Vue SPA的渲染流程:
```mermaid
graph TD
A[空白HTML] --> B[加载JS]
B --> C[执行Vue]
C --> D[生成DOM]
通过Chrome DevTools的Performance面板分析,主要耗时在: - 网络请求(特别是大型JS文件) - Vue实例初始化 - 组件递归渲染
在index.html中添加静态loading:
<div id="app-loading">
<div class="spinner"></div>
<p>应用加载中...</p>
</div>
<style>
#app-loading {
position: fixed;
/* 居中样式 */
animation: fadeIn 0.5s;
}
.spinner { /* 旋转动画实现 */ }
</style>
通过Vue mounted钩子移除:
new Vue({
mounted() {
const loading = document.getElementById('app-loading');
loading && loading.remove();
}
})
router.beforeEach((to, from, next) => {
showLoadingAnimation();
next();
});
router.afterEach(() => {
hideLoadingAnimation();
});
使用prerender-spa-plugin:
// vue.config.js
const PrerenderSPAPlugin = require('prerender-spa-plugin');
module.exports = {
configureWebpack: {
plugins: [
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: ['/', '/about'],
renderer: new PrerenderSPAPlugin.PuppeteerRenderer()
})
]
}
}
<!-- index.html -->
<div id="app">
<div class="skeleton-header"></div>
<div class="skeleton-body">
<div class="skeleton-line" v-for="i in 5"></div>
</div>
</div>
// skeleton.js
export default {
name: 'Skeleton',
template: `<!-- 骨架屏模板 -->`
}
// vue.config.js
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');
module.exports = {
configureWebpack: {
plugins: [
new SkeletonWebpackPlugin({
webpackConfig: {
entry: './src/skeleton.js'
}
})
]
}
}
Nuxt.js方案示例:
npx create-nuxt-app my-project
关键配置:
// nuxt.config.js
export default {
loading: '~/components/LoadingBar.vue',
loadingIndicator: {
name: 'circle',
color: '#3B8070'
}
}
/* 渐现动画 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 骨架屏动画 */
.skeleton-shimmer {
background: linear-gradient(
to right,
transparent 0%,
rgba(255,255,255,0.6) 50%,
transparent 100%
);
animation: shimmer 1.5s infinite;
}
安装lottie-web:
npm install lottie-web
组件封装:
<template>
<div ref="lottieContainer"></div>
</template>
<script>
import lottie from 'lottie-web';
export default {
props: ['animationData'],
mounted() {
this.anim = lottie.loadAnimation({
container: this.$refs.lottieContainer,
renderer: 'svg',
loop: true,
autoplay: true,
animationData: this.animationData
});
}
}
</script>
关键优化点: - 使用will-change属性 - 优先使用transform和opacity - 减少重排操作 - 使用requestAnimationFrame
function animate() {
// 使用硬件加速
element.style.transform = `translateY(${progress}px)`;
requestAnimationFrame(animate);
}
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
maxSize: 244 * 1024 // 244KB
}
}
}
}
<head>
<link rel="preload" href="/fonts/roboto.woff2" as="font">
<link rel="prefetch" href="/assets/logo.png">
</head>
组件级懒加载:
const Home = () => import(/* webpackChunkName: "home" */ './views/Home.vue');
图片懒加载:
<img v-lazy="imageSrc" alt="">
使用web-vitals库:
import { getLCP, getFID, getCLS } from 'web-vitals';
getLCP(console.log);
getFID(console.log);
getCLS(console.log);
// 记录白屏时间
const start = performance.now();
window.addEventListener('load', () => {
const timing = performance.now() - start;
axios.post('/monitor', { type: 'white_screen', timing });
});
graph LR
A[CDN] --> B[Nginx]
B --> C[SSR]
C --> D[Service Worker]
D --> E[Skeleton]
E --> F[Lazy Load]
// main.js
import App from './App.vue';
import store from './store';
import router from './router';
function bootstrap() {
// 显示加载动画
showLoading();
Promise.all([
store.dispatch('init'),
router.isReady()
]).then(() => {
const app = createApp(App);
app.mount('#app');
// 隐藏加载动画
hideLoading();
});
}
// 根据网络情况选择策略
if ('connection' in navigator) {
if (navigator.connection.saveData) {
loadLiteVersion();
} else {
bootstrap();
}
} else {
bootstrap();
}