防抖与节流:前端性能优化的“双胞胎兄弟”

0 阅读7分钟

防抖与节流:前端性能优化的“双胞胎兄弟”

在日常开发中,我们经常会遇到一些高频触发的事件,比如用户在搜索框里快速打字、疯狂滚动页面、频繁点击按钮等。如果不加以控制,这些操作可能会导致大量无意义的请求或计算,严重拖慢网页性能,甚至让服务器“喘不过气”。

这时候,防抖(Debounce)节流(Throttle) 就像一对默契配合的“双胞胎兄弟”,帮助我们优雅地控制函数执行频率,提升用户体验和系统性能。


什么是防抖?什么是节流?

  • 防抖(Debounce)
    连续触发某个事件时,只在事件停止触发一段时间后,才执行一次函数。
    👉 适用于“等用户操作结束再响应”的场景。
  • 节流(Throttle)
    连续触发事件的过程中,每隔固定时间就执行一次函数,无论中间触发了多少次。
    👉 适用于“按固定节奏响应”的场景。

它们不是互斥的,而是互补的工具——就像水龙头的两种开关方式:

  • 防抖是“你关紧了我才出水”;
  • 节流是“我每5秒滴一滴,不管你拧多快”。

两者都依赖 闭包 来保存状态(如定时器 ID、上次执行时间),并通过 setTimeout 控制执行时机,从而避免函数被无节制地调用。

一、为什么需要防抖?

想象一下你在百度搜索框里打字:“JavaScript”。如果你每按一个键,浏览器就立刻发一次请求去服务器查建议词,那短短8个字母就会触发8次网络请求!这不仅浪费带宽,还可能让服务器不堪重负。

<input type="text" id="undebounce" />
<script>
function ajax(content) {
    console.log('ajax request', content);
}
const inputa = document.getElementById('undebounce');
inputa.addEventListener('keyup', function (e) {
    ajax(e.target.value); // 每次按键都触发,太频繁!
})
</script>

简单运行一下:

未命名的设计 (9).gif

我们发现:

每次用户按下键盘(keyup 事件触发),都会立刻调用 ajax(e.target.value),导致请求过于频繁。

想象一下,如果你是用户:每按一个键,搜索建议就疯狂刷新、跳来跳去,还没看清结果就变了样——这不仅让人眼花缭乱影响体验,还可能因为请求延迟导致最终显示的是错误建议。

不仅性能极差,而且影响用户体验。这时候我们需要一种机制:等用户“停下来”再执行——这就是防抖的核心思想。


二、防抖(Debounce):只认“最后一次”

防抖 = 等你打完字再查

防抖的逻辑很简单:在一段时间内,如果事件被反复触发,就不断重置计时器;只有当事件停止触发超过指定时间后,才真正执行函数。

简单来说:

它会推迟执行函数,如果在这段时间内又被触发,就重置计时器,只执行最后一次的操作。

实现原理:闭包 + 定时器

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    const context = this;
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  };
}
  • fn:要防抖的函数(比如 ajax
  • delay:等待时间(比如 300 毫秒)
  • timer:保存在闭包中的定时器 ID,每次触发都清掉旧的,重新开始倒计时

这个函数的作用是:返回一个“被防抖包装过”的新函数,它在被频繁调用时,只会在最后一次调用之后等待 delay 毫秒,才真正执行原函数 fn

使用示例:

const input = document.getElementById('debounce');
input.addEventListener('keyup', debounce(function(e) {
  ajax(e.target.value);
}, 300));

现在,无论你多快打字,只要停顿超过 300 毫秒,才会真正发送请求。

例如: “天气预报”打完后手一停——只发一次请求,完美!

运行示例:

未命名的设计 (1).gif

适用场景

  • 搜索框输入建议
  • 窗口 resize 后重新计算布局
  • 表单提交前的验证(防止多次点击)

三、节流(Throttle):固定节奏,稳扎稳打

如果说防抖是“等你停下再做”,那节流就是“不管你多急,我按固定节奏来”。

防抖适合“最终结果”,但有些场景需要持续响应,比如监听页面滚动来实现“无限加载”。

如果滚动一下就请求一次,那滚动条一拉到底,可能瞬间触发上百次请求!

这时该节流出场了。

节流的核心思想

“不管你怎么闹,我每隔固定时间才理你一次。”

实现原理:记录上次执行时间

 function throttle(fn,delay){
            let 
            last,
            deferTimer;
            return function(){
                let that = this; // this丢失
                let _agrs = arguments; // 类数组对象
                let now = + new Date(); // 类型转换,毫秒数
                if(last && now < last + delay){
                 clearTimeout(deferTimer);
                deferTimer = setTimeout(function(){
                    last = now;
                    fn.apply(that,_agrs);
                },delay);
                }else{
                    last = now;
                    fn.apply(that,_agrs);
                }  
            } 
        }

这段 节流(throttle)函数 实现了一种“首次立即执行 + 后续延迟兜底”的混合策略。我们可以将其逻辑清晰地划分为 两种典型情况 来理解:

🟢 情况一:可以立即执行(冷却期已过 或 首次触发)

🔹 触发条件:
  • 第一次调用last0undefinedif 条件不成立);
  • 或距离上次执行已超过 delay 毫秒now >= last + delay)。
🔹 行为:
  • 立即执行 fn.apply(that, _args)
  • 更新 last = now,记录本次执行时间,开启新的冷却期。
🔹 用户体验:

“我一滚动,内容立刻加载!”——响应迅速,无延迟感。


🟡 情况二:还在冷却期(上次执行后未满 delay

🔹 触发条件:
  • last 已有值(不是第一次);
  • now < last + delay(当前时间仍在冷却窗口内)。
🔹 行为:
  1. 清除之前可能存在的延迟任务clearTimeout(deferTimer));
  2. 重新设置一个 setTimeout,在 剩余等待时间delay - (now - last))后执行;
  3. 执行时更新 last = now,并调用 fn

⚠️ 注意:这里 setTimeout 的延时是动态计算的,确保两次实际执行之间至少间隔 delay 毫秒

🔹 设计意图:
  • 不直接丢弃冷却期内的触发;
  • 而是“记住最后一次触发”,并在冷却结束后补一次执行
  • 避免用户快速操作被完全忽略(比如快速滚动到底部却没触发加载)。

🔄 两种情况对比总结

特征🟢 情况一:立即执行🟡 情况二:冷却期内
触发时机首次 or 距离上次 ≥ delay距离上次 < delay
是否立刻调用 fn✅ 是❌ 否(安排稍后执行)
last 更新时机立即更新setTimeout 中更新
用户体验即时响应操作不被丢弃,稍后补偿
典型场景页面首次滚动、窗口首次 resize快速连续滚动、高频 mousemove

使用示例:

const input = document.getElementById('throttle');
input.addEventListener('keyup', throttle(function(e) {
  ajax(e.target.value);
}, 300));

这样,无论用户怎么疯狂滚动,每300毫秒最多执行一次,既保证体验,又节省资源。

运行示例:

未命名的设计 (2).gif

适用场景

  • 页面滚动加载更多内容(scroll 事件)
  • 鼠标移动跟踪位置(mousemove)
  • 游戏中的技能冷却、自动攻击

四、防抖 vs 节流:关键区别

防抖(Debounce)节流(Throttle)
触发时机等你停止操作后才执行每隔固定时间就执行一次
执行次数一段时间内只执行最后一次一段时间内规律执行多次
典型场景搜索建议、窗口 resize、表单校验滚动加载、鼠标移动、按钮连点防护

五、总结:善用“双胞胎”,性能更优雅

  • 防抖:适合用户输入结束后的操作,追求“最终结果”,比如用户输入完毕后的搜索。
  • 节流:适合持续性高频事件,追求“稳定节奏”,比如持续滚动或移动时的反馈。

两者都利用了闭包 + 定时器的组合,通过控制函数执行频率,有效避免了性能浪费。掌握它们,你就拥有了前端性能优化的一对利器!

下次当你面对“疯狂触发”的事件时,不妨问问自己:

“我是要等他停下来再行动?还是每隔一阵子就响应一次?”

答案,就是选择防抖还是节流的关键。

学会合理使用防抖与节流,让你的网页既灵敏稳重,用户体验自然水涨船高!