骨架屏技术在前端性能优化中的实践与思考
先看效果,再看代码
我最近在优化一个内容型页面的加载体验,用户进来看到的是空白,等数据回来才渲染,体验很割裂。老板说“别的网站都有个骨架转圈,咱们也搞一个”。行吧,安排。
骨架屏不是什么高深技术,本质就是用 HTML + CSS 模拟出页面结构,在真实数据还没回来前先占个位。别小看这个细节,亲测有效提升感知性能——哪怕实际加载时间没变,用户觉得“这站不慢”。
最简单的做法?直接写个 div,给点灰条条,完事。但真项目里哪有这么简单。来,上实战代码:
<div class="skeleton-card">
<div class="skeleton-avatar"></div>
<div class="skeleton-content">
<div class="skeleton-line short"></div>
<div class="skeleton-line medium"></div>
<div class="skeleton-line long"></div>
</div>
</div>
.skeleton-card {
display: flex;
gap: 16px;
padding: 16px;
background: #fff;
border-radius: 8px;
border: 1px solid #eaeaea;
}
.skeleton-avatar {
width: 48px;
height: 48px;
background: #f0f0f0;
border-radius: 50%;
animation: pulse 1.5s infinite ease-in-out;
}
.skeleton-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-line {
height: 12px;
background: #f0f0f0;
border-radius: 4px;
animation: pulse 1.5s infinite ease-in-out;
}
.skeleton-line.short { width: 30%; }
.skeleton-line.medium { width: 60%; }
.skeleton-line.long { width: 80%; }
@keyframes pulse {
0% { opacity: 0.7; }
50% { opacity: 0.4; }
100% { opacity: 0.7; }
}
这段代码我在多个项目里复用过,改改尺寸就能用。注意那个 pulse 动画,不要用太花哨的渐变或者移动背景,低端机上会卡。我就见过有人用 background: linear-gradient(...) 配合 translateX 做流动光效,结果安卓 WebView 直接掉帧。
这个场景最好用
别一上来就全页面骨架。我试过给整个首页搞骨架,结果维护成本爆炸——UI一改,骨架也得跟着改,还不如不加。
真正值得上骨架的场景是:数据依赖远端接口、结构固定、用户停留时间长的地方。比如文章列表页、用户详情页、商品卡片这些。
拿文章列表举例,我的做法是封装一个组件,React/Vue 都能套:
// SkeletonPostList.jsx
import React from 'react';
const SkeletonPostList = ({ count = 5 }) => {
return Array.from({ length: count }).map((_, i) => (
<div key={i} className="skeleton-post-item">
<div className="skeleton-thumbnail"></div>
<div className="skeleton-post-content">
<div className="skeleton-line short" style={{ width: '40%' }}></div>
<div className="skeleton-line long"></div>
<div className="skeleton-line medium"></div>
</div>
</div>
));
};
export default SkeletonPostList;
.skeleton-post-item {
display: flex;
gap: 12px;
margin-bottom: 20px;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.skeleton-thumbnail {
width: 80px;
height: 60px;
background: #f0f0f0;
border-radius: 4px;
}
.skeleton-post-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
然后在真实组件里控制显隐:
// PostList.jsx
import { useState, useEffect } from 'react';
import SkeletonPostList from './SkeletonPostList';
const PostList = () => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://jztheme.com/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
return (
<div>
{loading ? (
<SkeletonPostList count={5} />
) : (
posts.map(post => <PostItem key={post.id} data={post} />)
)}
</div>
);
};
这里注意下,我踩过坑:一开始我把 loading 状态放在全局 store 里,结果其他地方改了 loading 状态,骨架屏莫名其妙消失。后来改成组件内独立管理,干净利落。
踩坑提醒:这三点一定注意
- 别让骨架屏比真实内容还复杂 —— 我见过团队把骨架做到和真实 DOM 节点数量一样多,CSS 类名都对齐。醒醒,这是浪费性能。骨架只要视觉近似就行,能对齐宽度高度就够了。
- 动画别太猛 —— 有些设计师非要加“从左到右扫过的光效”,技术上可以实现(用伪元素+animation),但在低端 Android 手机上会导致页面整体卡顿。建议只用简单的 opacity 脉冲动画,兼容性稳得多。
- 别忘了可访问性 —— 骨架屏对屏幕阅读器不友好。建议加上
aria-hidden="true",不然读屏软件会把一堆空 div 念出来,用户体验灾难。真实内容加载后也要及时移除 aria-hidden。
高级技巧:动态生成骨架
上面都是静态写死的骨架。如果你的列表项高度不固定,或者布局多变,可以考虑用 JS 动态生成。
比如根据当前可视区域计算需要多少个骨架项:
const getVisibleSkeletonCount = () => {
const itemHeight = 80; // 预估每个项高度
const containerHeight = window.innerHeight - 100; // 减去头部
return Math.ceil(containerHeight / itemHeight);
};
然后传给 Skeleton 组件:
<SkeletonPostList count={getVisibleSkeletonCount()} />
这样不会一次性渲染过多骨架节点,尤其在长列表中能省一点是一点。
更狠一点的做法是用 Intersection Observer 实现骨架懒加载,不过大多数场景没必要折腾到这程度。
服务端也能做?当然
骨架屏不止是前端的事。我在 SSR 项目里试过直接输出带骨架的 HTML,首屏不用等 JS 加载完就有占位结构。
比如 Node.js 后端模板里:
<!-- server-rendered.ejs -->
<div class="post-list">
<% if (loading) { %>
<div class="skeleton-post-item">...</div>
<div class="skeleton-post-item">...</div>
<% } else { %>
<% posts.forEach(post => { %>
<div class="post-item"><%= post.title %></div>
<% }); %>
<% } %>
</div>
这时候要注意前后端状态同步。我折腾了半天发现 hydration 错误,原因是服务端写的 loading=true,客户端刚上来也是 loading=true,但数据回来太快,React 认为 DOM 不一致,触发重渲染。解决方案是在客户端延迟显示真实内容 50ms,保证过渡自然。
CSS 方案 vs 组件方案
你可能会想:能不能只靠 CSS 控制显隐,不用写两套 JSX?
可以,但我不推荐。我试过:
.post-item {
visibility: hidden;
}
.post-item.loading::before {
content: '';
display: block;
/* 伪造骨架 */
}
问题是太难维护了。一旦结构变,CSS 得大改,而且无法复用。现在主流框架都支持组件化,直接写 <Skeleton /> 更直观,团队新人也能快速理解。
最后一点:别滥用
骨架屏不是万能药。如果接口本身特别慢(>3s),光靠骨架撑不住用户体验。该加 loading 提示加提示,该降级处理降级处理。
还有,纯展示类页面适合上骨架;工具类操作界面(比如表单、后台管理)其实没必要,用户更关心功能可用性,而不是“看起来快”。
以上是我个人对这个骨架屏的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

暂无评论