懒加载(Lazy Loading)是一种按需加载资源的优化策略:只有当资源真正需要展示时才发起请求,而不是页面初始化时就加载全部内容。对于包含大量图片、长列表或复杂组件的页面来说,懒加载能显著减少首屏请求数量、降低带宽消耗、加快可交互时间。本文将围绕图片懒加载和组件懒加载两条主线,系统讲解实现思路与具体代码。

一、图片懒加载:从滚动监听到Intersection Observer
图片懒加载的基本原理很简单:把真实的图片地址存放在data-src自定义属性中,src属性先不设置或使用占位图。当图片进入浏览器可视区域时,再把data-src的值赋给src,触发浏览器的下载行为。
最经典的实现方式是监听scroll事件,配合getBoundingClientRect计算图片相对于视口的位置:
// 获取所有需要懒加载的图片
const images = document.querySelectorAll('img[data-src]');
function lazyLoad() {
const viewportHeight = window.innerHeight;
images.forEach(img => {
const rect = img.getBoundingClientRect();
// 图片顶部进入视口时加载
if (rect.top < viewportHeight && rect.bottom > 0) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
}
window.addEventListener('scroll', throttle(lazyLoad, 200));
lazyLoad(); // 首次执行这种方案的问题在于scroll事件触发频率极高,即使用了节流函数,频繁调用getBoundingClientRect仍会造成主线程压力,同时还会引发强制重排(reflow),在低端设备上表现明显卡顿。
更优雅的方案是使用Intersection Observer API。它是浏览器原生提供的交叉观察器,会在目标元素与视口的交叉状态发生变化时异步回调,完全避免了滚动监听带来的性能问题:
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
obs.unobserve(img); // 加载一次后停止观察
}
});
}, {
root: null, // 以视口为参照
rootMargin: '0px 0px 200px 0px', // 提前200px预加载
threshold: 0.1 // 交叉比例达到10%时触发
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});其中rootMargin允许提前预加载即将进入视口的图片,避免用户快速滚动时看到空白;unobserve则确保每张图片只加载一次。此外,现代浏览器已支持原生的loading="lazy"属性,直接写在<img>标签上即可实现懒加载,无需任何JavaScript代码,但它的可定制性不如Intersection Observer,比如无法实现自定义占位动画和回调逻辑。
二、组件懒加载:动态import与框架级方案
图片之外,组件的延迟加载对打包体积优化同样关键。现代打包工具(如Webpack、Vite)支持动态import()语法,只有代码真正被执行时才会请求对应的chunk文件:
button.addEventListener('click', async () => {
// 点击时才加载弹窗组件
const { openDialog } = await import('./dialog.js');
openDialog();
});在React中,官方提供了React.lazy配合Suspense的组合方案。路由级别的懒加载是最常见的应用场景,用户访问哪个页面就加载哪个页面的代码:
import React, { Suspense, lazy } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}Vue的实现思路类似,通过defineAsyncComponent(Vue 3)或直接在路由配置中书写函数形式的component即可:
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/settings',
component: () => import('./views/Settings.vue') // 路由懒加载
}
];
const router = createRouter({
history: createWebHistory(),
routes
});组件懒加载的价值在于把单个大bundle拆分成多个小chunk,配合浏览器并行下载能力和HTTP缓存,用户首次访问的下载量可以减少一半以上,后续访问还能命中缓存秒开。
三、常见踩坑点与最佳实践
懒加载虽好,但使用不当会带来体验问题。首先是首屏图片不应懒加载:首屏内容直接加载反而更快,如果对首屏图片也做延迟处理,用户会先看到占位符再看到图片,观感变差。其次是SEO问题,搜索引擎爬虫不一定执行JavaScript,依赖JS的懒加载可能导致图片不被收录,重要内容建议使用原生loading="lazy"或直接输出真实src。
其次是占位符的处理。图片未加载时如果没有预留高度,加载完成后会引起布局抖动(CLS指标恶化)。推荐为图片明确设置宽高属性,或使用宽高比固定的容器、低清晰度模糊占位图(LQIP)来稳定布局:
<div style="aspect-ratio: 16 / 9; background: #eee;"> <img data-src="real-image.jpg" width="640" height="360" alt="示例图" /> </div>
最后是失败兜底与用户体验:网络请求可能失败,应监听error事件提供重试机制或降级占位图;对于懒加载的组件,要设计合理的loading状态,避免用户在弱网下长时间面对空白区域。整体而言,懒加载的收益取决于页面规模——内容越多、交互路径越深的应用,延迟加载带来的性能提升越明显,建议通过Lighthouse和Performance面板持续监测优化效果。
懒加载Intersection Observer图片延迟加载修改时间:2026-09-01 04:14:46