在前端开发中,JS懒加载成为提升首屏加载速度和用户体验的关键技术。本文详细介绍5种实现方式以及性能优化要点,帮助开发者在不同场景下选用合适方案。
实现方式一:原生属性 loading="lazy" 的懒加载
原理与适用场景
使用原生浏览器特性实现懒加载,无需额外 JavaScript 即可实现,非常适合图片和 iframe 等资源的简单场景。简洁实现,降低了维护成本,同时避免了额外脚本对首屏的阻塞。
该方案的核心要点是通过在元素上添加 loading="lazy" 属性,让浏览器在资源进入视口附近时再去加载,从而减少初始网络请求。对于简单图片展示的页面,这是一个快速且可靠的选项。
实现要点与示例
兼容性注意:该属性在大多数现代浏览器中得到支持,但早期浏览器可能并不支持,需在关键资源上考虑回退策略。
<img src="tiny-pixel.jpg" loading="lazy" alt="示例图片">
占位比例与替代资源:如果需要更好的降级策略,可以在 src 使用一个占位图片,后续再结合其他方案替换为真实资源,以避免页面空白区域。
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" loading="lazy" alt="示例图片">
性能要点与适用前提
性能提升的核心在于让浏览器自己决定何时下载资源,避免了初始渲染阶段的资源抢占,从而缩短首屏时间。对于图片数量有限、对降级容错要求较低的页面,这是最简单直接的懒加载实现。
在实现中应注意:若页面中存在大量图片,单独对关键区域使用原生懒加载即可满足需求,其他资源再通过 IntersectionObserver 等方式补充;此外,务必提供合适的占位资源,避免渲染时产生空白。
实现方式二:使用 IntersectionObserver 实现懒加载
原理与核心参数
IntersectionObserver 提供了对目标元素与祖先元素(或视窗)交叉情况的异步通知,无需轮询滚动事件,能高效地判断元素是否进入视口。
通过设置 rootMargin 与 threshold,可以控制触发时机(例如在元素进入视口前 200px 就开始加载)以及触发频率,获得更平滑的加载体验。
实现步骤与代码
步骤简述:选择需要懒加载的元素,创建一个 IntersectionObserver,将元素添加到观察列表。当元素进入视口时,读取 data-src 中的真实资源,替换到 src,并取消对该元素的观察。
// 伪代码
document.addEventListener('DOMContentLoaded', () => {const imgs = document.querySelectorAll('img.lazy');const obs = new IntersectionObserver((entries, observer) => {entries.forEach(entry => {if (entry.isIntersecting) {const img = entry.target;img.src = img.dataset.src;img.classList.remove('lazy');observer.unobserve(img);}});}, {rootMargin: '200px 0px',threshold: 0.01});imgs.forEach(img => obs.observe(img));
});
<img class="lazy" data-src="real-image.jpg" alt="..." src="placeholder.jpg">
性能要点与实现细节
避免回流与重绘:通过一次性加载触发,而非在滚动中频繁操作 DOM。rootMargin 与 threshold 的合理设置可以在用户到来之前就完成货品准备,提升感知性能。
对比原生懒加载,IntersectionObserver 的优势在于对大量资源的统一调度与可控性,适合较复杂的图片网格、卡片列表等场景。若浏览器兼容性是主诉求,可以结合一个降级逻辑在不支持该 API 的环境中使用其他方案。
实现方式三:滚动事件的节流/防抖实现懒加载
传统滚动加载的要点
在某些环境中,IntersectionObserver 不可用或兼容性不足时,可以通过滚动事件配合节流/防抖来实现懒加载。该方法的核心是判断图片与视口的相对位置,然后按需加载。
节流降低滚动处理函数的执行频率,避免大量的重排与重绘;防抖可确保在滚动结束后再执行加载判定,减少不必要的判断。
实现要点与示例
通过 requestAnimationFrame 或简单的时间戳阈值机制,确保在浏览器高效帧更新下完成判断。
// 节流实现懒加载
let lastCall = 0;
const throttle = (fn, delay) => {return () => {const now = Date.now();if (now - lastCall > delay) {lastCall = now;fn();}};
};const loadIfVisible = () => {const images = document.querySelectorAll('img.lazy');images.forEach(img => {const rect = img.getBoundingClientRect();if (rect.top < window.innerHeight + 200) {img.src = img.dataset.src;img.classList.remove('lazy');}});
};window.addEventListener('scroll', throttle(loadIfVisible, 200));
window.addEventListener('resize', throttle(loadIfVisible, 200));
let ticking = false;
function onScroll() {if (!ticking) {window.requestAnimationFrame(function() {loadIfVisible();ticking = false;});ticking = true;}
}
实现方式四:低质量占位符(LQIP)+ 渐进加载
概念与好处
通过使用极低分辨率的占位符图片或模糊效果,在真实图片还未加载完成前保持 UI 的占位状态,提升感知性能和用户体验。
该策略与渐进加载结合,可以让页面滚动时看到区域逐步填充,减少“空白等待”的焦虑感。
实现要点与示例
核心步骤:先加载占位图或模糊效果,随后替换为高分辨率图片,通常通过 data-src 或 srcset 实现。
<img class="lazy" src="image-lowres.jpg" data-src="image-highres.jpg" alt="..." />
// 替换高分辨率图片的简单逻辑(可结合 IntersectionObserver 使用)
function upgradeImage(img) {const high = new Image();high.onload = () => { img.src = high.src; };high.src = img.dataset.src;
}
document.addEventListener('DOMContentLoaded', () => {document.querySelectorAll('img.lazy').forEach(img => {// 假设已通过 IntersectionObserver 处理后加载img.addEventListener('lazyLoaded', () => upgradeImage(img));});
});
实现方式五:动态导入(dynamic import)实现 JS 懒加载
模块化与异步加载的结合
对于大型前端应用,以路由或用户交互为触发点动态导入模块,实现首屏体积最小化并按需加载功能性代码。
通过 import() 实现按需加载,结合打包工具的代码分割特性,能显著降低初始 JS 包大小,提升首屏渲染速度。
实现要点与示例
常见触发点包括按钮点击、进入特定路由、或滚动到特定区域。触发点执行 import('./module.js'),并在 then 或 async/await 的回调中初始化导出的 API。
document.querySelector('#loadFeature').addEventListener('click', async () => {const mod = await import('./modules/feature.js');mod.initFeature();
});
<button id="loadFeature">加载特性</button>
代码分割与预取:结合打包工具的代码分割特性,可对常用功能做预取(prefetch)或预加载(preload),进一步提升未来触发时的响应速度。



