4. 前端性能优化(三):懒加载与预加载
各位同学,今天我们来聊聊前端性能优化里非常实用的一招——懒加载与预加载。说实话,这两个概念听起来有点像「双胞胎」,但它们的应用场景和时机完全不同。我在项目中踩过不少坑,今天一并分享给你们。
4.1 图片懒加载:别让用户为看不见的东西买单
先问大家一个问题:一个页面有 100 张图片,用户只看了前 3 张,剩下的 97 张图片要不要加载?
答案显然是否定的。但很多新手项目就是这么干的——所有图片一股脑全请求。结果呢?首屏加载慢得像蜗牛,用户早跑了。
图片懒加载的核心思想:只有当图片进入视口(viewport)时,才去加载它。说白了,就是「按需加载」。
4.1.1 传统实现方式:监听滚动事件
我记得早期做电商项目时,用的就是监听 scroll 事件 + 计算元素位置。代码大概长这样:
function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
}
window.addEventListener('scroll', throttle(lazyLoad, 200));
嗯,这里要注意:一定要加节流(throttle)。不然滚动一次触发几十次计算,性能反而更差。我当年就吃过这个亏,页面卡得不行,后来加了 200ms 的节流才解决。
4.1.2 现代方案:Intersection Observer API
现在浏览器提供了更优雅的 API——IntersectionObserver。它就像一个「哨兵」,自动帮我们监测元素是否进入视口。
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
4.1.3 占位与过渡
图片懒加载还有一个容易被忽略的点——占位。如果图片还没加载出来,页面布局会「跳动」,用户体验很差。
我个人习惯的做法:
- 先用一个低分辨率的模糊图或纯色背景占位
- 等原图加载完成后,做一个淡入过渡
img {
opacity: 0;
transition: opacity 0.3s ease;
}
img.loaded {
opacity: 1;
}
你想想看,用户看到图片从模糊变清晰,是不是比突然「蹦」出来舒服多了?
4.2 路由懒加载:按需加载,别一次全塞给用户
单页应用(SPA)有个通病:所有页面代码打包成一个巨大的 JS 文件。用户访问首页,却把「个人中心」「设置页」「帮助页」的代码全下载了。这合理吗?
当然不合理。路由懒加载就是解决这个问题的——访问哪个路由,才加载哪个路由的代码。
4.2.1 Vue 中的实现
在 Vue 项目中,我通常这样写:
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
];
这里用了动态 import(),Webpack 或 Vite 会自动把它拆分成独立的 chunk。用户访问 /home 时,只加载 Home 的代码。
webpackPrefetch 或 webpackPreload 来优化。
4.2.2 React 中的实现
React 里用 React.lazy + Suspense:
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
注意那个 fallback,它是加载过程中显示的占位内容。我建议不要只写「Loading...」,可以放一个骨架屏或者进度条,用户体验会好很多。
4.3 资源预加载:提前准备,让用户感觉「秒开」
懒加载是「晚点加载」,预加载正好相反——提前加载用户可能需要的资源。
举个例子:用户鼠标 hover 到一个按钮上,大概率会点击。这时候提前加载按钮对应的页面,等用户真正点击时,页面已经准备好了。
4.3.1 使用 <link rel="preload">
这是浏览器原生支持的预加载方式:
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.webp" as="image">
preload 告诉浏览器:「这个资源很重要,尽快加载」。它适合加载首屏关键资源,比如字体、首屏大图、关键 CSS。
4.3.2 使用 <link rel="prefetch">
prefetch 则更温和一些:
<link rel="prefetch" href="next-page.js">
它告诉浏览器:「有空的话,帮我把这个资源加载了」。浏览器会在空闲时间下载,不影响当前页面性能。
4.3.3 智能预加载策略
我在项目中用过一种「预测式预加载」:根据用户行为预测下一步操作。
- 用户鼠标悬停在「下一页」按钮上 → 预加载下一页内容
- 用户滚动到页面底部 → 预加载「相关文章」
- 用户填写表单 → 预加载「提交成功」页面
代码实现也不复杂:
document.querySelector('.next-btn').addEventListener('mouseenter', () => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = '/next-page';
document.head.appendChild(link);
});
嗯,这里要注意:别过度预加载。我曾经在一个项目里预加载了 10 多个页面,结果用户只看了 2 个,白白浪费了流量和带宽。预加载要「精准」,而不是「贪多」。
4.4 懒加载 vs 预加载:什么时候用哪个?
很多同学会问:这两个到底怎么选?我画了一张图,帮大家理清思路:
说白了,核心就一句话:「现在要用的,赶紧加载;将来要用的,提前准备;不确定的,等到了再说」。
4.5 实战建议与避坑指南
- 课程封面图 → 懒加载(用户滚动到才加载)
- 视频播放页 → 预加载(用户点击「开始学习」前就加载)
- 首页关键 CSS → preload(保证首屏渲染速度)
- 别对移动端用户过度预加载,流量很贵
- 懒加载的图片要设置宽高,防止布局抖动
- 路由懒加载的 chunk 命名要有规律,方便调试
- 预加载不要和懒加载冲突——同一个资源别既预加载又懒加载
好了,这一章的内容就到这里。记住:性能优化不是「炫技」,而是让用户用得更舒服。懒加载和预加载就像「精打细算的管家」,知道什么时候该花钱,什么时候该省钱。你学会了吗?
公众号:蓝海资料掘金营,微信deep3321