JavaScript 性能优化:防抖和节流

前言

作为一位AI全栈开发学习者,最近在复习高频事件的性能优化时,又把防抖和节流翻出来啃了一遍,越啃越觉得这俩东西虽然小,但用好了真的能省下不少资源,还能让交互体验更丝滑。

今天就把我的学习笔记 + 代码实践整理成一篇干货,力求讲得透彻、讲得接地气,希望能帮到正在被 input 搜索、scroll 加载、resize 这些场景折磨的你。

为什么需要防抖和节流?

先说最常见的痛点:

想象一下,你在做一个搜索框,用户每敲一个字就立刻发一次 AJAX 请求去后端拿搜索建议。

  • 用户打字很快,一句话 10 个字,可能触发 10 次请求。
  • 后端压力大,带宽浪费严重。
  • 大部分请求其实是"无效"的,因为用户还在继续输入,前几次的结果用户根本看都不看。

再比如页面滚动加载更多:

  • scroll 事件触发频率极高(浏览器一秒可能触发几十次)。
  • 每次滚动都去判断是否触底、发请求加载数据,性能开销巨大,页面容易卡顿。

类似场景还有:

  • window.resize
  • mousemove
  • 代码编辑器的自动保存或智能提示
  • 按钮防止重复点击

这些事件有一个共性:触发太频繁,而我们真正关心的往往只是其中一部分触发

这时候,防抖和节流就派上用场了,它们是前端性能优化的"降频神器"。

防抖(debounce):只关心"最后一次"

核心思想

"管你触发多少次,我只认最后一次,在你安静下来一段时间后我再执行。"

形象点说:就像你妈叫你下楼吃饭,你一直喊"等会儿",直到你连续 2 秒没喊,她才真的相信你马上就下去。

经典场景

  1. 搜索框输入建议(百度、淘宝搜索框)
  2. 表单实时校验(用户名是否可用)
  3. 代码编辑器智能提示
  4. 按钮防止短时间内重复提交

实现原理

利用定时器 + 闭包:

  • 每次事件触发时,先把之前的定时器清除掉
  • 重新开启一个新的定时器,delay 毫秒后执行目标函数
  • 如果在 delay 时间内又触发了事件,就再清除、再重开
  • 只有当连续 delay 毫秒内没有新触发,才真正执行
JavaScript 复制代码
function debounce(fn, delay) {
  let timer = null; // 借助闭包保存定时器 ID

  return function (...args) {
    // 每次触发时先把上一次的定时器干掉
    if (timer) {
      clearTimeout(timer);
    }

    // 重新开一个定时器,延迟执行
    timer = setTimeout(() => {
      fn.apply(this, args); // 保证 this 和参数正确传递
      timer = null; // 执行完可以选择清空,方便 GC
    }, delay);
  };
}

立即执行版防抖(重要变种)

有时候我们希望第一次触发就立刻执行,之后在冷却时间内不重复执行(比如按钮防重点击)。

JavaScript 复制代码
function debounce(fn, delay, immediate = false) {
  let timer = null;

  return function (...args) {
    if (timer) clearTimeout(timer);

    if (immediate && !timer) {
      fn.apply(this, args); // 第一次立即执行
    }

    timer = setTimeout(() => {
      if (!immediate) {
        fn.apply(this, args);
      }
      timer = null;
    }, delay);
  };
}

节流(throttle):每隔一段时间就执行一次

核心思想

"不管你触发多快,我每隔固定时间只执行一次。"

就像游戏里按住自动射击的枪,射速是固定的,再怎么狂按鼠标也超不过这个频率。

经典场景

  1. 滚动加载更多(scroll 事件)
  2. 高频点击按钮做动画反馈
  3. mousemove 拖拽
  4. 游戏中的技能冷却

实现原理(时间戳版)

记录上次执行的时间戳,这次触发时判断是否已经过了 delay,如果过了就立刻执行,并更新时间戳。

JavaScript 复制代码
function throttle(fn, delay) {
  let last = 0; // 上次执行时间

  return function (...args) {
    const now = Date.now();

    if (now - last >= delay) {
      fn.apply(this, args);
      last = now;
    }
  };
}

优点:第一次触发会立即执行,最后一次如果刚好在时间窗口内也会执行。

实现原理(定时器版)

用定时器控制执行频率,适合需要"严格每 delay 毫秒执行一次"的场景。

JavaScript 复制代码
function throttle(fn, delay) {
  let timer = null;

  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

缺点:第一次不会立即执行,最后一次触发后还要等 delay 才执行。

推荐:时间戳 + 定时器混合版(最实用)

兼顾开头立即执行 + 结尾也能执行

JavaScript 复制代码
function throttle(fn, delay) {
  let last = 0;
  let timer = null;

  return function (...args) {
    const now = Date.now();

    // 剩余时间
    const remain = delay - (now - last);

    if (remain <= 0) {
      // 已经超过间隔时间
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, args);
      last = now;
    } else if (!timer) {
      // 在间隔内,但还没设过定时器,就补一次
      timer = setTimeout(() => {
        fn.apply(this, args);
        last = Date.now();
        timer = null;
      }, remain);
    }
  };
}

防抖 vs 节流,一表看懂区别

防抖 (debounce) 节流 (throttle)
执行时机 停止触发后 delay 毫秒才执行一次 每隔 delay 毫秒执行一次
执行次数 多次触发只执行最后一次 多次触发会执行多次(间隔执行)
典型场景 输入搜索、表单校验 滚动加载、拖拽、射击游戏
比喻 等你说完再回消息 水龙头每秒只出固定水量

总结

  • 高频事件不优化 = 性能杀手
  • 防抖:适合"等用户操作结束再处理"的场景(如搜索、提交)
  • 节流:适合"需要持续反馈但又不能太频繁"的场景(如滚动、拖拽)
  • 两者都依赖闭包保存定时器/时间戳,是闭包最经典的应用之一
  • 实际项目中优先考虑使用成熟库(如 lodash.debounce/throttle),自己手写主要是为了面试和加深理解
相关推荐
hxjhnct1 分钟前
Vue 实现多行文本“展开收起”
前端·javascript·vue.js
百锦再3 分钟前
Vue大屏开发全流程及技术细节详解
前端·javascript·vue.js·微信小程序·小程序·架构·ecmascript
麻瓜呀4 分钟前
vue2之el-table表格多选改单选
javascript·vue.js·elementui
龙仔CLL8 分钟前
vue3使用node-rsa报错buffer is not defined
javascript·vue.js
进击的小头9 分钟前
18_C语言算法面试与进阶:高频算法题实战与学习路线规划
c语言·算法·面试
黑土豆12 分钟前
一次真实的流式踩坑:fetchEventSource vs fetch流读取的本质区别
前端·javascript·ai编程
donecoding15 分钟前
告别 scrollIntoView 的“越级滚动”:一行代码解决横向滚动问题
前端·javascript
0__O15 分钟前
如何在 monaco 中实现自定义语言的高亮
前端·javascript·编程语言
呆头鸭L18 分钟前
快速上手Electron
前端·javascript·electron
秋天的一阵风22 分钟前
🌟 藏在 Vue3 源码里的 “二进制艺术”:位运算如何让代码又快又省内存?
前端·vue.js·面试