本文通过具体代码实现与应用实例,深入讲解JavaScript中的防抖、节流与图片懒加载技术,分析执行时机、边界处理与现代浏览器最佳实践。
JavaScript性能优化实例:防抖节流与懒加载代码实现
时间:2026-10-30
浏览:639
文章目录
前端界面中诸如窗口缩放(resize)、页面滚动(scroll)、输入补全(input)等高频事件,若直接绑定复杂的业务逻辑或 DOM 计算,会导致短时间内执行海量冗余脚本,引发掉帧、卡顿甚至浏览器未响应。解决这一性能瓶颈的关键手段是控制高频操作的执行频率,以及延后非首屏重资源的加载时机。本文将逐一给出防抖(Debounce)、节流(Throttle)和图片懒加载(Lazy Load)的实际生产级代码,拆解其运作机理与边界细节,帮助读者在具体场景中正确选型与落地。
一、高频事件的调度机理:防抖与节流
浏览器的事件循环在主线程空闲时处理微任务与重绘。当用户快速输入或快速滚动页面时,事件触发频率可高达每秒几十甚至上百次。直接在事件回调中触发重排(Reflow)、重绘(Repaint)或发送网络请求,会严重堵塞主线程。
防抖和节流都是利用 JavaScript 闭包维护状态,通过时间阈值来截流调用的函数封装技术,但它们的核心控制哲学截然不同:
- 防抖(Debounce):将多次连续触发“合并为最后一次”。在设定的延迟时间内,如果事件被再次触发,则重新计时,直到事件彻底停止触发并超过指定时间后才执行回调。
- 节流(Throttle):将高频触发“稀释为固定频率”。在固定的时间间隔内,无论事件触发多少次,回调函数最多只执行一次,保证过程按节奏平缓推进。
二、防抖函数(Debounce)的代码实现
编写防抖函数时,除了维护基础定时器清空逻辑外,还需处理两个关键工程问题:
- 保持原函数的
this与参数传递:确保事件对象event及其上下文不发生丢失。 - 支持首发立即执行(Immediate / Leading):在某些如提交表单、避免重复点击按钮的场景中,第一次触发需要立刻做出响应,后续连续点击才进行拦截。
下面是一个包含立即执行控制和取消(cancel)支持的完整防抖实现:
/**
* 完整防抖函数
* @param {Function} fn 目标业务函数
* @param {number} delay 延迟等待时间(毫秒)
* @param {boolean} immediate 是否在第一次触发时立即执行
* @returns {Function} 包装后的防抖函数
*/
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
function debounced(...args) {
const context = this;
// 清除上一次未到期的定时器
if (timer) {
clearTimeout(timer);
}
if (immediate) {
// 若当前没有定时器在运行,说明处于静止状态后的第一次触发
const callNow = !timer;
// 设置定时器,在等待期过后清空 timer,恢复下一次可立即执行的状态
timer = setTimeout(() => {
timer = null;
}, delay);
if (callNow) {
fn.apply(context, args);
}
} else {
// 常规防抖:等待静止期结束后执行
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
}
// 提供取消机制,供组件卸载或路由切换时清理资源
debounced.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
return debounced;
}
debounce(fn),否则每次重新渲染都会生成全新的闭包与定时器,导致防抖失效。在 React 中应结合 useCallback 或自定义 Hook;在 Vue 中应挂载到 methods 或 onMounted 中。
三、节流函数(Throttle)的代码实现
节流的实现通常有两种策略:时间戳法和定时器法。它们各具特点:
- 时间戳实现:事件触发时对比当前时间与上一次执行时间差。大于间隔即执行。特点是第一次事件立即触发,但最后一次事件停止后不会有追加补尝(无尾部执行)。
- 定时器实现:触发时若无定时器则设定延时。特点是首次不会立即执行,而在间隔末期执行,且最后一次事件能够得到保障(有尾部执行)。
为了兼顾两者的优势,生产级方案通常提供 leading(是否首次执行)和 trailing(是否尾部补调)配置项。以下是首尾可控的节流完整实现:
/**
* 完整节流函数(支持首尾执行控制)
* @param {Function} fn 目标业务函数
* @param {number} interval 节流周期(毫秒)
* @param {Object} options 配置项 { leading: boolean, trailing: boolean }
* @returns {Function} 包装后的节流函数
*/
function throttle(fn, interval = 200, options = { leading: true, trailing: true }) {
let previous = 0;
let timer = null;
const { leading, trailing } = options;
function throttled(...args) {
const context = this;
const now = Date.now();
// 如果不允许首次立即执行,且上一次执行时间为0,则将 previous 初始化为当前时间
if (!previous && !leading) {
previous = now;
}
const remaining = interval - (now - previous);
// 已经超过了设定的间隔周期
if (remaining <= 0 || remaining > interval) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(context, args);
previous = now;
} else if (!timer && trailing) {
// 处于间隔期内,若开启了 trailing,则保留最后一次调用的定时器
timer = setTimeout(() => {
previous = !leading ? 0 : Date.now();
timer = null;
fn.apply(context, args);
}, remaining);
}
}
throttled.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
previous = 0;
};
return throttled;
}
四、图片懒加载原理与代码实现
在包含大量图片的长列表中,直接加载所有资源会导致初次加载时并发请求激增,争抢关键 CSS/JS 资源带宽,造成页面首屏白屏时间拉长。图片懒加载通过将真实图片地址保存在 data-src 自定义属性中,只在图片即将或已经进入可视区域时,才将地址写入 src 属性发起实际网络请求。
1. 基于 IntersectionObserver 的现代懒加载实现
现代浏览器提供了原生 IntersectionObserver API,该 API 将元素与视口的交叉检测交由浏览器底层负责,以异步方式触发回调,完全不会阻塞主线程,无需手动计算位置或挂载节流函数。
/**
* 现代图片懒加载初始化器
* @param {string} selector 图片选择器,例如 'img.lazy-img'
* @param {Object} options IntersectionObserver 配置
*/
function initModernLazyLoad(selector = 'img.lazy-img', options = {}) {
// 如果浏览器不支持原生 IntersectionObserver,自动降级
if (!('IntersectionObserver' in window)) {
initLegacyLazyLoad(selector);
return;
}
const defaultOptions = {
root: null, // 默认为顶级视口
rootMargin: '100px 0px', // 提前 100px 预加载,避免用户滑到空白
threshold: 0.01 // 出现 1% 即触发
};
const observerConfig = Object.assign(defaultOptions, options);
const observer = new IntersectionObserver((entries, currentObserver) => {
entries.forEach(entry => {
// 当目标元素进入视口或设定的预加载阈值范围
if (entry.isIntersecting) {
const img = entry.target;
const realSrc = img.dataset.src;
if (realSrc) {
// 预载图片,确认下载成功后再替换,避免闪烁或破图
const tempImg = new Image();
tempImg.src = realSrc;
tempImg.onload = () => {
img.src = realSrc;
img.removeAttribute('data-src');
img.classList.add('loaded');
};
tempImg.onerror = () => {
img.classList.add('error');
};
}
// 停止对当前已处理图片的观察,释放性能
currentObserver.unobserve(img);
}
});
}, observerConfig);
// 绑定所有目标图片
const targets = document.querySelectorAll(selector);
targets.forEach(img => observer.observe(img));
}
2. 基于 Scroll 与节流的传统降级方案
对于不支持交叉观察器的老旧环境,需结合 scroll、resize 事件以及 getBoundingClientRect(),并必须使用我们前文实现的 throttle 函数包裹,否则会引起严重的滚动掉帧。
/**
* 传统滚动监听降级方案
* @param {string} selector 图片选择器
*/
function initLegacyLazyLoad(selector = 'img.lazy-img') {
let images = Array.from(document.querySelectorAll(selector));
const checkImages = () => {
if (images.length === 0) {
window.removeEventListener('scroll', throttledCheck);
window.removeEventListener('resize', throttledCheck);
return;
}
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const preloadBuffer = 100; // 预加载缓冲量
images = images.filter(img => {
const rect = img.getBoundingClientRect();
// 判断元素是否已出现在视口及其缓冲带内
const isInViewport = rect.top <= windowHeight + preloadBuffer && rect.bottom >= -preloadBuffer;
if (isInViewport) {
const realSrc = img.dataset.src;
if (realSrc) {
img.src = realSrc;
img.removeAttribute('data-src');
img.classList.add('loaded');
}
return false; // 从数组中移除已加载项
}
return true; // 保留尚未进入视口的项
});
};
// 使用前面封装的节流函数,固定 150ms 检查一次
const throttledCheck = throttle(checkImages, 150);
window.addEventListener('scroll', throttledCheck, { passive: true });
window.addEventListener('resize', throttledCheck, { passive: true });
// 页面首次渲染时主动触发一次
checkImages();
}
五、技术选型决策与组合实战
在复杂的前端业务中,开发者往往面临“用防抖还是用节流”、“懒加载该选原生属性还是代码实现”的选择困惑。下图展示了高频事件与加载优化的选型决策逻辑:
六、综合实战排坑与注意事项
在实际项目落地中,细节往往决定了优化的成败。以下梳理了三项技术在实战中最常见的陷阱与排查要点:
| 技术项 | 常见踩坑点 | 产生后果 | 正确应对策略 |
|---|---|---|---|
| 防抖 (Debounce) | 组件重新渲染导致闭包重新生成 | 每次事件都拿到新函数,防抖完全失效 | 使用 useCallback 或模块级作用域固定闭包实例 |
| 节流 (Throttle) | 漏掉 trailing: true 尾部处理 |
用户最后一次操作未得到执行,导致数据不一致 | 配置首尾支持,在间隔结束后执行最后一次入参调用 |
| 懒加载 (Lazy Load) | 图片没有提前占据固定宽高或占位容器 | 图片加载后撑开高度,造成严重累积布局偏移(CLS) | 设置 aspect-ratio 或骨架屏占位容器保持结构稳定 |
| 内存管理 | 页面或单页路由销毁未移除监听或解绑观察者 | DOM 已从视图脱离但闭包常驻,引起内存泄漏 | 在组件销毁钩子中调用 .cancel() 或 unobserve() |
七、总结
防抖、节流与懒加载是前端性能优化中最经典也是最高效的三个实战手段。总结其核心规律:
- 面对连续动作,若只需要在用户彻底停下后获取最终结果,采用防抖(Debounce);
- 面对持续过程,若需要在过程中按一定节拍保持响应与刷新,采用节流(Throttle);
- 面对大量长列表重媒体资源,优先采用现代
IntersectionObserver进行异步懒加载,并配合预加载缓冲与尺寸占位,消除页面卡顿与抖动。
通过本文提供的可配置实现方案,开发者可以在实际项目中直接应用,并根据具体业务需求弹性调整延时、阈值与首尾触发逻辑,实现平滑、稳定、高效的用户交互体验。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。