打开一个电商详情页,图片还没加载完,标题、价格、按钮一股脑从上往下蹦出来,页面高度反复跳动,这是很多用户都经历过的糟糕体验。骨架屏的思路很简单:在真实数据到达之前,先用一组灰色色块按照内容的真实布局占据空间,等数据回来后再平滑替换。因为骨架的尺寸和真实内容一致,渲染完成时不会产生布局抖动。本文介绍在React项目中实现骨架屏的几种常见做法,从手写组件到第三方库,再到与Suspense配合的进阶用法。

一、手写一个骨架屏组件
骨架屏本质上就是一组带动画的占位块,核心是两件事:色块形状要贴合真实内容,动画要轻量流畅。最常用的动画是shimmer效果,也就是一道高光从左到右扫过,营造内容正在加载的感觉。实现方式是用一个渐变背景配合background-position的位移动画,只操作transform可以避免触发重排,性能开销很小。
先写通用的样式。这里用CSS的linear-gradient画出一个倾斜的高光条,再通过keyframes让它无限循环移动:
.skeleton {
position: relative;
overflow: hidden;
background-color: #e5e7eb;
border-radius: 4px;
}
.skeleton::after {
content: '';
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.35) 50%,
rgba(255, 255, 255, 0) 100%
);
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}样式做好后,封装一个React组件,让它可以根据传入的宽高渲染出任意尺寸的色块,这样头像、标题、段落都能复用:
function Skeleton({ width, height, style }) {
return (
<span
className="skeleton"
style={{ width, height, display: 'block', ...style }}
/>
);
}
// 组合出一个文章卡片骨架
function ArticleCardSkeleton() {
return (
<div style={{ padding: 16, border: '1px solid #eee' }}>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Skeleton width={48} height={48} style={{ borderRadius: '50%' }} />
<div style={{ flex: 1 }}>
<Skeleton width="60%" height={14} />
<Skeleton width="30%" height={12} style={{ marginTop: 8 }} />
</div>
</div>
<Skeleton width="100%" height={180} style={{ marginTop: 12 }} />
<Skeleton width="90%" height={12} style={{ marginTop: 12 }} />
<Skeleton width="75%" height={12} style={{ marginTop: 6 }} />
</div>
);
}使用时只需要根据loading状态切换渲染内容。注意骨架的宽高要尽量和真实内容一致,比如头像固定48像素就写死48,标题通常一行就按一行的高度来。尺寸对得越准,替换瞬间的跳动越小。如果某些内容高度本身不确定(比如多行文本),可以按最常见的行数估算,宁可略小也不要留大片空白。
二、使用react-loading-skeleton库
手写方案胜在可控,但如果项目里占位场景很多,逐个调尺寸会很繁琐。react-loading-skeleton是一个专门做这件事的库,它把色块、动画、组合布局都封装好了,API非常简洁。安装命令:
npm install react-loading-skeleton
它的Skeleton组件默认会继承父元素的宽度,高度通过height指定,circle属性可以直接生成圆形头像占位。对于多行文本,用count属性就能一次性生成多行,而且最后一行会自动缩短一些,看起来更像真实段落:
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/index.css';
function ProfileSkeleton() {
return (
<div>
<Skeleton circle height={56} width={56} />
<Skeleton height={18} width="50%" style={{ marginTop: 10 }} />
<Skeleton count={4} height={14} style={{ marginTop: 8 }} />
</div>
);
}这个库的优势在于细节处理得比手写更周到:动画方向、颜色、圆角、高光宽度都可以通过主题统一配置,多个骨架块的动画还能自动错开时间,视觉上更有节奏感。缺点是引入了额外依赖,深度定制需要覆盖它的CSS变量。一般来说,中小项目手写一次全局样式就够了,占位场景复杂或对动效有统一要求时再考虑引入库。
三、结合Suspense与懒加载的进阶用法
前面两种方式都需要手动管理loading状态,也就是在组件里写三元表达式判断渲染骨架还是真实内容。当项目使用React 18的Suspense配合数据请求库(比如react-query、Relay或者自己用use实现)时,可以把骨架屏声明式地放进fallback,代码会干净很多:
import { Suspense, use } from 'react';
function ProductPage({ id }) {
return (
<Suspense fallback={<ArticleCardSkeleton />}>
<ArticleCard id={id} />
</Suspense>
);
}
function ArticleCard({ id }) {
// 数据未就绪时Suspense会展示fallback,就绪后自动渲染真实内容
const data = use(readArticlePromise(id));
return <div>{data.title}</div>;
}这种模式下,业务组件完全不用关心加载态,只负责渲染数据,骨架屏统一挂在Suspense的fallback上。路由级的应用也很常见:配合React.lazy做代码分割时,给每个懒加载的路由组件包一层Suspense,fallback放对应页面的骨架,用户切换页面时看到的就不再是白屏。
还有几点实践建议值得注意。第一,骨架屏展示时间不宜过长,如果接口普遍要两秒以上,应该优先考虑请求缓存或数据预加载,骨架只是缓冲手段而不是根治方案。第二,切换到真实内容时可以加一个短暂的淡入动画让替换更自然,但时长控制在200毫秒以内,否则会显得拖沓。第三,记得给骨架元素加上aria-hidden属性,并通过prefers-reduced-motion媒体查询关闭动画,避免对读屏设备和无障碍用户造成干扰。把这些细节处理好,骨架屏才能真正起到提升感知性能的作用。
React骨架屏Skeleton Screen加载占位修改时间:2026-09-13 00:26:46