前端性能优化实用方案(三):骨架屏提升30%用户感知速度
3 骨架屏和Loading状态管理:让等待变得不那么焦虑
用户打开页面看到白屏,心里会想什么?“这网站是不是挂了?”、“我的网络有问题吗?”、“要不要刷新一下?”
这种焦虑感会在3秒内达到顶峰。如果这时候还是白屏,用户很可能就直接关掉了。
骨架屏就是为了解决这个问题而生的。它不能让页面加载更快,但能让用户感觉没那么慢。研究数据显示,合理使用骨架屏可以让用户感知的加载速度提升20-30%。
3.1 骨架屏的核心思路
骨架屏本质上是一种心理学技巧。当用户看到页面的基本轮廓时,大脑会认为"内容正在加载",而不是"什么都没有"。
这就像排队买奶茶,如果你能看到前面还有几个人,心里就有数了。但如果什么都看不到,你会觉得等了很久。
3.2 基础骨架屏实现
先来看看最基本的骨架屏是怎么做的。
CSS动画效果
/* 基础骨架屏样式 */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* 不同形状的骨架屏 */
.skeleton-text {
height: 16px;
border-radius: 4px;
margin-bottom: 8px;
}
.skeleton-text.title {
height: 24px;
width: 60%;
}
.skeleton-text.subtitle {
height: 18px;
width: 80%;
}
.skeleton-text.content {
height: 14px;
width: 100%;
}
.skeleton-text.short {
width: 40%;
}
.skeleton-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.skeleton-image {
width: 100%;
height: 200px;
border-radius: 8px;
}
.skeleton-button {
width: 120px;
height: 36px;
border-radius: 6px;
}
.skeleton-card {
padding: 16px;
border-radius: 8px;
border: 1px solid #e0e0e0;
margin-bottom: 16px;
}
/* 深色主题适配 */
@media (prefers-color-scheme: dark) {
.skeleton {
background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
}
.skeleton-card {
border-color: #404040;
}
}
这套CSS的关键在于那个流动的渐变动画。1.5秒的循环时间经过测试,既不会太快让人眼花,也不会太慢显得卡顿。
3.3 Vue组件化骨架屏
把骨架屏做成组件,用起来就方便多了。
<template>
<div class="skeleton-container">
<!-- 用户信息骨架屏 -->
<div v-if="type === 'user'" class="skeleton-user">
<div class="skeleton skeleton-avatar"></div>
<div class="skeleton-user-info">
<div class="skeleton skeleton-text title"></div>
<div class="skeleton skeleton-text subtitle"></div>
</div>
</div>
<!-- 文章列表骨架屏 -->
<div v-else-if="type === 'article'" class="skeleton-article">
<div class="skeleton skeleton-image"></div>
<div class="skeleton-article-content">
<div class="skeleton skeleton-text title"></div>
<div class="skeleton skeleton-text content"></div>
<div class="skeleton skeleton-text content"></div>
<div class="skeleton skeleton-text short"></div>
<div class="skeleton-article-meta">
<div class="skeleton skeleton-avatar"></div>
<div class="skeleton skeleton-text short"></div>
</div>
</div>
</div>
<!-- 卡片列表骨架屏 -->
<div v-else-if="type === 'card'" class="skeleton-card">
<div class="skeleton skeleton-text title"></div>
<div class="skeleton skeleton-text content"></div>
<div class="skeleton skeleton-text content"></div>
<div class="skeleton skeleton-text short"></div>
<div class="skeleton-card-footer">
<div class="skeleton skeleton-button"></div>
<div class="skeleton skeleton-text short"></div>
</div>
</div>
<!-- 表格骨架屏 -->
<div v-else-if="type === 'table'" class="skeleton-table">
<div class="skeleton-table-header">
<div v-for="n in columns" :key="n" class="skeleton skeleton-text"></div>
</div>
<div v-for="row in rows" :key="row" class="skeleton-table-row">
<div v-for="n in columns" :key="n" class="skeleton skeleton-text"></div>
</div>
</div>
<!-- 自定义骨架屏 -->
<div v-else class="skeleton-custom">
<slot></slot>
</div>
</div>
</template>
<script setup>
const props = defineProps({
type: {
type: String,
default: 'custom',
validator: (value) => ['user', 'article', 'card', 'table', 'custom'].includes(value)
},
rows: {
type: Number,
default: 3
},
columns: {
type: Number,
default: 4
}
});
</script>
<style scoped>
.skeleton-container {
padding: 16px;
}
.skeleton-user {
display: flex;
align-items: center;
gap: 12px;
}
.skeleton-user-info {
flex: 1;
}
.skeleton-article {
display: flex;
flex-direction: column;
gap: 12px;
}
.skeleton-article-content {
flex: 1;
}
.skeleton-article-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
}
.skeleton-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
}
.skeleton-table {
width: 100%;
}
.skeleton-table-header,
.skeleton-table-row {
display: grid;
grid-template-columns: repeat(var(--columns, 4), 1fr);
gap: 12px;
margin-bottom: 8px;
}
.skeleton-table-header {
padding-bottom: 8px;
border-bottom: 1px solid #e0e0e0;
}
</style>
这个组件的设计思路是预设几种常见的页面布局。实际项目中,90%的情况都能用这几种类型搞定。
3.4 React版本的骨架屏
React的实现思路差不多,但有些细节不同。
import React from 'react';
import './skeleton.css';
const SkeletonLoader = ({
type = 'custom',
count = 1,
rows = 3,
columns = 4,
children
}) => {
const renderSkeleton = () => {
switch (type) {
case 'user':
return (
<div className="skeleton-user">
<div className="skeleton skeleton-avatar"></div>
<div className="skeleton-user-info">
<div className="skeleton skeleton-text title"></div>
<div className="skeleton skeleton-text subtitle"></div>
</div>
</div>
);
case 'article':
return (
<div className="skeleton-article">
<div className="skeleton skeleton-image"></div>
<div className="skeleton-article-content">
<div className="skeleton skeleton-text title"></div>
<div className="skeleton skeleton-text content"></div>
<div className="skeleton skeleton-text content"></div>
<div className="skeleton skeleton-text short"></div>
</div>
</div>
);
case 'card':
return (
<div className="skeleton-card">
<div className="skeleton skeleton-text title"></div>
<div className="skeleton skeleton-text content"></div>
<div className="skeleton skeleton-text content"></div>
<div className="skeleton skeleton-text short"></div>
</div>
);
case 'table':
return (
<div className="skeleton-table">
<div
className="skeleton-table-header"
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
>
{Array.from({ length: columns }, (_, i) => (
<div key={i} className="skeleton skeleton-text"></div>
))}
</div>
{Array.from({ length: rows }, (_, rowIndex) => (
<div
key={rowIndex}
className="skeleton-table-row"
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
>
{Array.from({ length: columns }, (_, colIndex) => (
<div key={colIndex} className="skeleton skeleton-text"></div>
))}
</div>
))}
</div>
);
default:
return children || <div className="skeleton skeleton-text"></div>;
}
};
return (
<div className="skeleton-container">
{Array.from({ length: count }, (_, index) => (
<div key={index}>
{renderSkeleton()}
</div>
))}
</div>
);
};
// 高阶组件:为任何组件添加骨架屏
const withSkeleton = (WrappedComponent, skeletonProps = {}) => {
return function SkeletonWrapper(props) {
const { loading, ...restProps } = props;
if (loading) {
return <SkeletonLoader {...skeletonProps} />;
}
return <WrappedComponent {...restProps} />;
};
};
export default SkeletonLoader;
export { withSkeleton };
withSkeleton这个高阶组件很实用。你可以给任何组件包一层,自动处理loading状态。
3.5 智能骨架屏生成器
手动写骨架屏有点麻烦,能不能自动生成?当然可以。
// 自动生成骨架屏的工具类
class SkeletonGenerator {
constructor() {
this.skeletonMap = new WeakMap();
}
// 分析DOM结构并生成对应的骨架屏
generateFromElement(element) {
if (this.skeletonMap.has(element)) {
return this.skeletonMap.get(element);
}
const skeleton = this.createSkeletonStructure(element);
this.skeletonMap.set(element, skeleton);
return skeleton;
}
createSkeletonStructure(element) {
const rect = element.getBoundingClientRect();
const computedStyle = window.getComputedStyle(element);
const skeletonElement = document.createElement('div');
skeletonElement.className = 'skeleton';
// 复制基本样式
skeletonElement.style.width = rect.width + 'px';
skeletonElement.style.height = rect.height + 'px';
skeletonElement.style.borderRadius = computedStyle.borderRadius;
skeletonElement.style.margin = computedStyle.margin;
skeletonElement.style.padding = computedStyle.padding;
// 根据元素类型调整样式
const tagName = element.tagName.toLowerCase();
switch (tagName) {
case 'img':
skeletonElement.classList.add('skeleton-image');
break;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
skeletonElement.classList.add('skeleton-text', 'title');
break;
case 'p':
case 'span':
case 'div':
if (rect.height <= 30) {
skeletonElement.classList.add('skeleton-text');
} else {
skeletonElement.classList.add('skeleton-block');
}
break;
case 'button':
skeletonElement.classList.add('skeleton-button');
break;
default:
skeletonElement.classList.add('skeleton-block');
}
return skeletonElement;
}
// 为整个容器生成骨架屏
generateForContainer(container) {
const skeletonContainer = document.createElement('div');
skeletonContainer.className = 'skeleton-container';
const elements = container.querySelectorAll('img, h1, h2, h3, h4, h5, h6, p, button, [data-skeleton]');
elements.forEach(element => {
if (this.isVisible(element)) {
const skeleton = this.generateFromElement(element);
skeletonContainer.appendChild(skeleton);
}
});
return skeletonContainer;
}
// 检查元素是否可见
isVisible(element) {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return (
rect.width > 0 &&
rect.height > 0 &&
style.visibility !== 'hidden' &&
style.display !== 'none' &&
style.opacity !== '0'
);
}
// 应用骨架屏到页面
applyToPage(selector = 'body') {
const containers = document.querySelectorAll(selector);
containers.forEach(container => {
const skeleton = this.generateForContainer(container);
// 隐藏原内容
container.style.visibility = 'hidden';
// 插入骨架屏
container.parentNode.insertBefore(skeleton, container);
// 标记以便后续移除
skeleton.setAttribute('data-skeleton-for', container.id || 'anonymous');
});
}
// 移除骨架屏
removeSkeletons() {
const skeletons = document.querySelectorAll('.skeleton-container');
skeletons.forEach(skeleton => {
const targetId = skeleton.getAttribute('data-skeleton-for');
if (targetId && targetId !== 'anonymous') {
const target = document.getElementById(targetId);
if (target) {
target.style.visibility = 'visible';
}
}
skeleton.remove();
});
}
}
// 使用示例
const skeletonGenerator = new SkeletonGenerator();
// 页面加载时应用骨架屏
document.addEventListener('DOMContentLoaded', () => {
skeletonGenerator.applyToPage('.main-content');
});
// 数据加载完成后移除骨架屏
window.addEventListener('load', () => {
setTimeout(() => {
skeletonGenerator.removeSkeletons();
}, 500);
});
export default SkeletonGenerator;
这个生成器的思路是分析现有DOM结构,然后生成对应的骨架屏。虽然不如手写的精确,但对于快速原型开发很有用。
3.6 Loading状态的全局管理
项目大了之后,各种loading状态会很混乱。需要一个统一的管理方案。
// 全局加载状态管理器
class LoadingManager {
constructor() {
this.loadingStates = new Map();
this.globalLoading = false;
this.listeners = new Set();
}
// 设置加载状态
setLoading(key, isLoading, options = {}) {
const prevState = this.loadingStates.get(key);
if (isLoading) {
this.loadingStates.set(key, {
startTime: Date.now(),
message: options.message || '加载中...',
type: options.type || 'default',
timeout: options.timeout || 30000
});
// 设置超时
if (options.timeout) {
setTimeout(() => {
if (this.loadingStates.has(key)) {
console.warn(`Loading timeout for key: ${key}`);
this.setLoading(key, false);
}
}, options.timeout);
}
} else {
this.loadingStates.delete(key);
}
// 更新全局加载状态
this.updateGlobalLoading();
// 通知监听器
this.notifyListeners(key, isLoading, prevState);
}
// 获取加载状态
isLoading(key) {
return this.loadingStates.has(key);
}
// 获取所有加载状态
getAllLoadingStates() {
return Object.fromEntries(this.loadingStates);
}
// 更新全局加载状态
updateGlobalLoading() {
const wasLoading = this.globalLoading;
this.globalLoading = this.loadingStates.size > 0;
if (wasLoading !== this.globalLoading) {
this.toggleGlobalLoadingUI(this.globalLoading);
}
}
// 切换全局加载UI
toggleGlobalLoadingUI(show) {
let loadingOverlay = document.getElementById('global-loading-overlay');
if (show && !loadingOverlay) {
loadingOverlay = this.createLoadingOverlay();
document.body.appendChild(loadingOverlay);
} else if (!show && loadingOverlay) {
loadingOverlay.remove();
}
}
// 创建加载遮罩
createLoadingOverlay() {
const overlay = document.createElement('div');
overlay.id = 'global-loading-overlay';
overlay.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
`;
// 添加样式
const style = document.createElement('style');
style.textContent = `
#global-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(2px);
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #666;
font-size: 14px;
}
`;
if (!document.getElementById('loading-styles')) {
style.id = 'loading-styles';
document.head.appendChild(style);
}
return overlay;
}
// 添加监听器
addListener(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
// 通知监听器
notifyListeners(key, isLoading, prevState) {
this.listeners.forEach(callback => {
try {
callback(key, isLoading, prevState);
} catch (error) {
console.error('Loading listener error:', error);
}
});
}
// 批量操作
batch(operations) {
operations.forEach(({ key, loading, options }) => {
this.setLoading(key, loading, options);
});
}
// 清除所有加载状态
clearAll() {
this.loadingStates.clear();
this.updateGlobalLoading();
}
}
// 创建全局实例
const loadingManager = new LoadingManager();
// 便捷方法
export const showLoading = (key, options) => loadingManager.setLoading(key, true, options);
export const hideLoading = (key) => loadingManager.setLoading(key, false);
export const isLoading = (key) => loadingManager.isLoading(key);
export default loadingManager;
这个管理器的好处是可以同时处理多个loading状态,还能设置超时自动取消。在复杂的单页应用中特别有用。
3.7 实际项目中的应用
看看在真实项目中怎么用这些技术。
<template>
<div class="dashboard">
<!-- 用户信息区域 -->
<div class="user-section">
<SkeletonLoader
v-if="userLoading"
type="user"
/>
<UserProfile
v-else
:user="userInfo"
/>
</div>
<!-- 文章列表区域 -->
<div class="articles-section">
<h2>最新文章</h2>
<div v-if="articlesLoading" class="articles-skeleton">
<SkeletonLoader
type="article"
:count="3"
/>
</div>
<ArticleList
v-else
:articles="articles"
/>
</div>
<!-- 数据统计区域 -->
<div class="stats-section">
<h2>数据统计</h2>
<div v-if="statsLoading" class="stats-skeleton">
<SkeletonLoader
type="card"
:count="4"
/>
</div>
<StatsCards
v-else
:stats="statsData"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import SkeletonLoader from '@/components/SkeletonLoader.vue';
import { showLoading, hideLoading } from '@/utils/loadingManager';
const userLoading = ref(true);
const articlesLoading = ref(true);
const statsLoading = ref(true);
const userInfo = ref(null);
const articles = ref([]);
const statsData = ref(null);
// 模拟数据加载
const loadUserInfo = async () => {
showLoading('user-info', { message: '加载用户信息...' });
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
userInfo.value = {
name: '张三',
email: 'zhangsan@example.com',
avatar: '/avatars/user1.jpg'
};
} finally {
userLoading.value = false;
hideLoading('user-info');
}
};
const loadArticles = async () => {
showLoading('articles', { message: '加载文章列表...' });
try {
await new Promise(resolve => setTimeout(resolve, 1500));
articles.value = [
{ id: 1, title: '文章1', content: '内容1' },
{ id: 2, title: '文章2', content: '内容2' },
{ id: 3, title: '文章3', content: '内容3' }
];
} finally {
articlesLoading.value = false;
hideLoading('articles');
}
};
const loadStats = async () => {
showLoading('stats', { message: '加载统计数据...' });
try {
await new Promise(resolve => setTimeout(resolve, 800));
statsData.value = {
totalViews: 12345,
totalArticles: 56,
totalComments: 789,
totalLikes: 234
};
} finally {
statsLoading.value = false;
hideLoading('stats');
}
};
// 并行加载数据
onMounted(async () => {
await Promise.all([
loadUserInfo(),
loadArticles(),
loadStats()
]);
});
</script>
<style scoped>
.dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.user-section,
.articles-section,
.stats-section {
margin-bottom: 32px;
}
.articles-skeleton,
.stats-skeleton {
display: grid;
gap: 16px;
}
.stats-skeleton {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
</style>
这个例子展示了如何在实际项目中组合使用骨架屏和loading管理。不同区域可以独立加载,用户体验会好很多。
3.8 性能监控和优化
骨架屏做好了,还要知道效果怎么样。
// 骨架屏性能监控
class SkeletonMetrics {
constructor() {
this.metrics = {
skeletonShowTime: new Map(),
contentLoadTime: new Map(),
userPerception: new Map()
};
}
// 记录骨架屏显示时间
recordSkeletonShow(key) {
this.metrics.skeletonShowTime.set(key, performance.now());
}
// 记录内容加载完成时间
recordContentLoad(key) {
const showTime = this.metrics.skeletonShowTime.get(key);
if (showTime) {
const loadTime = performance.now() - showTime;
this.metrics.contentLoadTime.set(key, loadTime);
// 分析用户感知性能
this.analyzeUserPerception(key, loadTime);
}
}
// 分析用户感知性能
analyzeUserPerception(key, loadTime) {
let perception;
if (loadTime < 1000) {
perception = 'excellent'; // 优秀
} else if (loadTime < 3000) {
perception = 'good'; // 良好
} else if (loadTime < 5000) {
perception = 'acceptable'; // 可接受
} else {
perception = 'poor'; // 较差
}
this.metrics.userPerception.set(key, {
loadTime,
perception,
timestamp: Date.now()
});
// 发送到分析服务
this.sendToAnalytics(key, loadTime, perception);
}
// 发送数据到分析服务
sendToAnalytics(key, loadTime, perception) {
// 这里可以发送到Google Analytics、百度统计等
if (typeof gtag !== 'undefined') {
gtag('event', 'skeleton_performance', {
event_category: 'UX',
event_label: key,
value: Math.round(loadTime),
custom_parameter_1: perception
});
}
}
// 获取性能报告
getPerformanceReport() {
const report = {
totalSamples: this.metrics.contentLoadTime.size,
averageLoadTime: 0,
perceptionDistribution: {
excellent: 0,
good: 0,
acceptable: 0,
poor: 0
}
};
// 计算平均加载时间
const loadTimes = Array.from(this.metrics.contentLoadTime.values());
if (loadTimes.length > 0) {
report.averageLoadTime = loadTimes.reduce((sum, time) => sum + time, 0) / loadTimes.length;
}
// 统计感知分布
this.metrics.userPerception.forEach(({ perception }) => {
report.perceptionDistribution[perception]++;
});
return report;
}
// 自动优化建议
getOptimizationSuggestions() {
const report = this.getPerformanceReport();
const suggestions = [];
if (report.averageLoadTime > 3000) {
suggestions.push('平均加载时间超过3秒,建议优化数据加载逻辑');
}
if (report.perceptionDistribution.poor > report.totalSamples * 0.2) {
suggestions.push('超过20%的加载被用户感知为较差,建议检查网络请求');
}
if (report.perceptionDistribution.excellent < report.totalSamples * 0.5) {
suggestions.push('优秀感知比例不足50%,可以考虑预加载关键数据');
}
return suggestions;
}
}
// 使用示例
const skeletonMetrics = new SkeletonMetrics();
// 在显示骨架屏时记录
const showSkeletonWithMetrics = (key, type) => {
skeletonMetrics.recordSkeletonShow(key);
// 显示骨架屏的逻辑
};
// 在内容加载完成时记录
const hideSkeletonWithMetrics = (key) => {
skeletonMetrics.recordContentLoad(key);
// 隐藏骨架屏的逻辑
};
// 定期生成报告
setInterval(() => {
const report = skeletonMetrics.getPerformanceReport();
const suggestions = skeletonMetrics.getOptimizationSuggestions();
console.log('骨架屏性能报告:', report);
console.log('优化建议:', suggestions);
}, 60000); // 每分钟生成一次报告
export default SkeletonMetrics;
3.9 一些实用的小技巧
渐进式加载
不要一次性显示所有骨架屏,可以分批出现,更自然。
// 渐进式显示骨架屏
const showSkeletonsProgressively = (containers, delay = 100) => {
containers.forEach((container, index) => {
setTimeout(() => {
container.classList.add('skeleton-visible');
}, index * delay);
});
};
// CSS配合
.skeleton-container {
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
}
.skeleton-container.skeleton-visible {
opacity: 1;
transform: translateY(0);
}
智能预测加载时间
根据历史数据预测加载时间,动态调整骨架屏显示策略。
class LoadingPredictor {
constructor() {
this.history = JSON.parse(localStorage.getItem('loading-history') || '{}');
}
// 记录加载时间
recordLoadTime(key, time) {
if (!this.history[key]) {
this.history[key] = [];
}
this.history[key].push(time);
// 只保留最近10次记录
if (this.history[key].length > 10) {
this.history[key] = this.history[key].slice(-10);
}
localStorage.setItem('loading-history', JSON.stringify(this.history));
}
// 预测加载时间
predictLoadTime(key) {
const records = this.history[key];
if (!records || records.length === 0) {
return 2000; // 默认2秒
}
// 计算加权平均,最近的记录权重更高
let weightedSum = 0;
let totalWeight = 0;
records.forEach((time, index) => {
const weight = index + 1; // 越新的记录权重越高
weightedSum += time * weight;
totalWeight += weight;
});
return Math.round(weightedSum / totalWeight);
}
// 根据预测时间调整骨架屏策略
getSkeletonStrategy(key) {
const predictedTime = this.predictLoadTime(key);
if (predictedTime < 500) {
return { showSkeleton: false, reason: '预计加载很快,不显示骨架屏' };
} else if (predictedTime < 1500) {
return { showSkeleton: true, type: 'simple', reason: '显示简单骨架屏' };
} else {
return { showSkeleton: true, type: 'detailed', reason: '显示详细骨架屏' };
}
}
}
const predictor = new LoadingPredictor();
// 使用示例
const smartLoadWithSkeleton = async (key, loadFunction) => {
const strategy = predictor.getSkeletonStrategy(key);
const startTime = performance.now();
if (strategy.showSkeleton) {
showSkeleton(key, strategy.type);
}
try {
const result = await loadFunction();
const loadTime = performance.now() - startTime;
predictor.recordLoadTime(key, loadTime);
return result;
} finally {
if (strategy.showSkeleton) {
hideSkeleton(key);
}
}
};
小结
骨架屏和Loading状态管理看起来是小细节,但对用户体验的影响很大。
核心要点:
- 骨架屏不是为了让页面加载更快,而是让等待不那么焦虑
- 组件化设计让骨架屏更容易维护和复用
- 全局loading管理避免状态混乱
- 性能监控帮助持续优化用户体验
- 智能策略让骨架屏更加人性化
实施建议:
- 先从最关键的页面开始,不要一次性改造所有页面
- 骨架屏的形状要尽量接近真实内容
- 加载时间超过3秒的一定要有进度提示
- 定期分析用户行为数据,调整策略
下一篇我们会讲缓存策略和资源预加载,进一步提升首屏性能。
- 点赞
- 收藏
- 关注作者
评论(0)