组件懒加载策略深度解析
从路由懒加载到可见性加载,详解组件懒加载的三层策略、IntersectionObserver原理和骨架屏最佳实践
一句话概括
懒加载的本质是把「用户不需要立刻看到」的代码推迟到「用户需要时」再下载——路由懒加载是骨架(按页面拆分),组件可见性加载是血肉(滚到才加载),骨架屏 + prefetch 是灵魂(加载不闪屏、点击不等待)。
核心知识点
1. 路由懒加载:最高 ROI 的一刀
一笔投入,效果最猛。每个页面独立 chunk,不访问就不下载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// React Router v6:路由懒加载标准写法
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home'))
const Admin = lazy(() => import('./pages/Admin'))
const Profile = lazy(() => import('./pages/Profile'))
function App() {
return (
<Suspense fallback={<PageSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin" element={<Admin />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
)
}
1
2
3
4
5
// Vue Router 同理,甚至更简洁
const routes = [
{ path: '/admin', component: () => import('./views/Admin.vue') },
{ path: '/profile', component: () => import('./views/Profile.vue') },
]
一个 1.5MB 的 SPA 做完路由懒加载后,首屏通常降到 300KB 以内——这是优化投入产出比最高的一步。
2. 可见性加载:IntersectionObserver 精准触达
页面底部的评论区、推荐模块——用户可能永远不会滚到那。用 IntersectionObserver 在这些模块「即将进入视口」时才加载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { useRef, useState, useEffect } from 'react'
function LazySection({ children, fallback, rootMargin = '200px' }) {
const ref = useRef(null)
const [shouldLoad, setShouldLoad] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setShouldLoad(true)
observer.disconnect() // 只触发一次,加载后停止观察
}
},
{ rootMargin } // 提前 200px 触发,下载和渲染无缝衔接
)
observer.observe(el)
return () => observer.disconnect()
}, [rootMargin])
return (
<div ref={ref}>
{shouldLoad ? children : fallback}
</div>
)
}
// 使用
<LazySection rootMargin="300px" fallback={<CommentSkeleton />}>
<CommentSection />
</LazySection>
为什么不用 scroll 事件? scroll 回调需要 getBoundingClientRect() 强制回流,高频触发时性能灾难;IntersectionObserver 是浏览器异步 API,在 GPU 合成线程做计算,不阻塞主线程。
3. 图片懒加载:原生 loading="lazy" + 渐进式
现代浏览器已经原生支持图片懒加载,一行 HTML 即可:
1
2
3
4
5
<!-- 原生懒加载:Chrome 77+、Safari 15.4+ 已支持 -->
<img src="banner.jpg" loading="lazy" />
<!-- iframe 也支持 -->
<iframe src="embed.html" loading="lazy" />
但生产环境常用增强方案——占位图 + 渐进式加载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function LazyImage({ src, alt }) {
const [loaded, setLoaded] = useState(false)
return (
<div style={{ position: 'relative', background: '#f0f0f0' }}>
{!loaded && <div className="blur-placeholder" />} {/* 模糊占位 */}
<img
src={src}
alt={alt}
loading="lazy"
onLoad={() => setLoaded(true)}
style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
</div>
)
}
4. 骨架屏:加载不是 bug,加载时的体验差距才是
懒加载最怕的是:用户滚到一个区域 → 白屏闪烁 → 内容突然蹦出来(布局抖动)。骨架屏解决的就是这段等待期的体感:
1
2
3
4
5
6
7
8
9
10
/* 骨架屏动画 */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
核心原则:骨架屏的尺寸必须和真实内容一致(或设置 min-height),否则内容出现时页面会跳动——比白屏几秒还让用户难受。
其实你每天都在用
- 路由分割:你打开淘宝首页不会下载「订单详情页」的代码——这就是路由懒加载。
loading="lazy":你用<img loading="lazy">时,浏览器自动判断图片离视口距离决定何时下载。- webpackChunkName 魔法注释:你在
import()里写的/* webpackChunkName: "admin" */控制着产物文件名,方便定位和缓存策略。 - IntersectionObserver 不仅是懒加载:它也被用于埋点曝光统计(「用户是否看到了这个广告位」)和无限滚动。
- 骨架屏 vs Spinner:YouTube、Twitter 都在用骨架屏而非转圈 spinner——因为它给用户「内容马上就来」的心理暗示,而不是「还在加载」。
常见误解(FAQ)
❌ 误区:「懒加载就是性能优化,做得越细越好」
过度拆分会适得其反——每个 chunk 都是一个 HTTP 请求。30 个 5KB 的 chunk 比 1 个 150KB 的 chunk 在 HTTP/1.1 下慢得多(并发限制 + RTT 开销)。合理的拆分:每个 chunk 30-150KB,vendor 单独抽出。
❌ 误区:「React.lazy 加载失败,fallback 会一直显示」
不会。chunk 加载失败时 React 会 throw error,Suspense 被设计成只处理加载中状态,失败必须用 ErrorBoundary 接住并展示重试按钮——生产环境 lazy + Suspense + ErrorBoundary 是铁三角。
❌ 误区:「只要做好懒加载就不需要关心 bundle 大小了」
懒加载解决的是加载时机问题,不是代码体积问题。一个 500KB 的 chunk 懒加载了还是 500KB——该砍的依赖还是要砍(tree shaking、按需引入),该压缩的还是要压缩。
❌ 误区:「IntersectionObserver 的 rootMargin 设越大越好」
rootMargin 设太大会导致页面一加载就触发大量观察回调,虽然「提前了」但可能影响首屏渲染。一般建议:关键内容 200-300px,尾部内容 0-100px,平衡预加载和性能。
一句话总结
懒加载的目标不是「能不加载就不加载」,而是让用户永远感觉不到「正在加载」——骨架屏、rootMargin、prefetch 的组合拳,比单纯的「晚下载」更重要。