页面加载性能直接决定了用户的第一印象。资源预加载和懒加载是两种互补的优化手段:懒加载推迟非关键资源的加载时机,减少首屏负担;预加载提前获取即将用到的资源,让后续交互瞬间完成。合理搭配使用,能让应用既快又流畅。
懒加载的核心手段
1. 路由级代码分割
在 React 中,React.lazy 和 Suspense 是最基础的懒加载工具。将页面组件通过动态 import() 引入,每个页面会被打包成独立的 chunk,只有访问时才加载。
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
2. 组件级按需加载
并非只有路由才需要懒加载。大型的弹窗、图表库、富文本编辑器等非首屏直接可见的组件,都可以用同样的方式拆分。
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function ReportPage() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>查看图表</button>
{showChart && (
<Suspense fallback={<div>图表加载中...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
3. 图片懒加载
对于长列表或包含大量图片的页面,浏览器原生的 loading="lazy" 属性是最简单有效的方式。
function ImageGallery({ images }) {
return (
<div className="gallery">
{images.map(img => (
<img
key={img.id}
src={img.src}
alt={img.alt}
loading="lazy" // 浏览器原生懒加载
width={img.width}
height={img.height}
/>
))}
</div>
);
}
对于需要更精细控制(如占位符、加载动画)的场景,可使用 IntersectionObserver 实现自定义懒加载逻辑,或直接使用 react-lazyload 等成熟库。
预加载的实用策略
1. 鼠标悬停或意图预加载
当用户将鼠标悬停在某个链接或按钮上时,可以趁机预加载目标页面的代码,这样真正点击时页面几乎瞬间呈现。
function Navigation() {
const preloadDashboard = () => {
// 动态 import,Webpack/Vite 会自动分离 chunk
import('./pages/Dashboard');
};
return (
<nav>
<Link to="/" onMouseEnter={preloadDashboard}>仪表盘</Link>
</nav>
);
}
这种方式不需要你手动操作 <link> 标签,构建工具会处理好一切。Vite 和 Webpack 都支持动态 import 的自动分割。
2. 关键资源的 Preload 和 Prefetch
<link rel="preload">:告诉浏览器“当前页面马上要用这个资源,最高优先级下载”。适合首屏必须的字体文件、关键 CSS 或主要图片。<link rel="prefetch">:告诉浏览器“这个资源将来可能会用到,空闲时提前下载”。适合下一页面可能用到的 JS 或数据。
在 React 应用中,可以通过 React Helmet 或 Next.js 的 <Head> 组件动态插入这些标签:
import { Helmet } from 'react-helmet';
function ProductPage() {
return (
<>
<Helmet>
{/* 预加载首屏关键图片 */}
<link rel="preload" href="/hero-banner.jpg" as="image" />
{/* 预取下一步很可能访问的页面 */}
<link rel="prefetch" href="/checkout.js" as="script" />
</Helmet>
{/* 页面内容 */}
</>
);
}
3. 数据预加载
除了代码和静态资源,数据也可以提前获取。TanStack Query 提供了 prefetchQuery 方法,可以在用户即将访问某个页面时预加载数据,减少等待时间。
import { useQueryClient } from '@tanstack/react-query';
function UserList() {
const queryClient = useQueryClient();
const prefetchUser = (userId) => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
};
return (
<ul>
{users.map(user => (
<li
key={user.id}
onMouseEnter={() => prefetchUser(user.id)}
>
<Link to={`/user/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}
当用户悬停列表项时,用户详情的数据已经在后台加载,点击跳转后立刻就有缓存数据可用。
懒加载与预加载的配合原则
- 首屏优先,其他推迟:首屏所需资源使用
preload确保尽早到位,非首屏内容懒加载处理。 - 预判用户行为:利用悬停、滚动到接近区域等信号触发预加载,时机在你判断“用户极大概率会点击”时。
- 避免过度预加载:预加载会占用网络带宽和 CPU,如果用户最终没有使用,就是浪费。要有选择性地预加载,而不是一股脑全部 prefetch。
- 利用工具自动分析:Next.js 等框架会自动为页面中的
<Link>组件预取目标页面资源,无需手写任何预加载代码。纯 React 项目则需要根据路由库和环境自行配置。
实际开发中的常见问题与处理
1. React.lazy 不支持命名导出
如果你引入的模块使用了命名导出而非默认导出,需要用中间对象包裹:
// 错误:如果 OtherComponent 是命名导出
const OtherComponent = lazy(() => import('./OtherComponent'));
// 正确:转换一下
const OtherComponent = lazy(() =>
import('./OtherComponent').then(module => ({ default: module.OtherComponent }))
);
2. 组件懒加载导致的闪动问题
Suspense 的 fallback 和实际组件渲染切换可能导致布局抖动。确保 fallback 与实际组件占据相同的空间,或者使用 useTransition 让旧内容保持显示直到新内容准备好。
3. 构建工具配置
在 Vite 中,动态 import 默认自动分割 chunk,无需额外配置。如果需要对 chunk 分组或命名,可以使用魔法注释:
const Dashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard'));
// Vite 暂不支持 webpack 魔法注释,但可通过其他方式分组
Vite 的 chunk 分割策略基于 Rollup,可以在 vite.config.js 中通过 output.manualChunks 控制。
通过有意识的懒加载和预加载策略,你可以在保持应用功能完整的同时,将首屏加载时间缩短到用户可接受的 2-3 秒以内,后续交互也能保持极快的响应速度。这两个技术是前端性能优化中最容易落地且收益最明显的方案之一。