JavaScript性能优化实例:防抖节流与懒加载代码实现

作者:性能诊所时间:2026-10-30 18:54:03浏览:640

本文通过具体代码实现与应用实例,深入讲解JavaScript中的防抖、节流与图片懒加载技术,分析执行时机、边界处理与现代浏览器最佳实践。

前端界面中诸如窗口缩放(resize)、页面滚动(scroll)、输入补全(input)等高频事件,若直接绑定复杂的业务逻辑或 DOM 计算,会导致短时间内执行海量冗余脚本,引发掉帧、卡顿甚至浏览器未响应。解决这一性能瓶颈的关键手段是控制高频操作的执行频率,以及延后非首屏重资源的加载时机。本文将逐一给出防抖(Debounce)、节流(Throttle)和图片懒加载(Lazy Load)的实际生产级代码,拆解其运作机理与边界细节,帮助读者在具体场景中正确选型与落地。

一、高频事件的调度机理:防抖与节流

浏览器的事件循环在主线程空闲时处理微任务与重绘。当用户快速输入或快速滚动页面时,事件触发频率可高达每秒几十甚至上百次。直接在事件回调中触发重排(Reflow)、重绘(Repaint)或发送网络请求,会严重堵塞主线程。

防抖和节流都是利用 JavaScript 闭包维护状态,通过时间阈值来截流调用的函数封装技术,但它们的核心控制哲学截然不同:

  • 防抖(Debounce):将多次连续触发“合并为最后一次”。在设定的延迟时间内,如果事件被再次触发,则重新计时,直到事件彻底停止触发并超过指定时间后才执行回调。
  • 节流(Throttle):将高频触发“稀释为固定频率”。在固定的时间间隔内,无论事件触发多少次,回调函数最多只执行一次,保证过程按节奏平缓推进。
JavaScript性能优化实例:防抖节流与懒加载代码实现
图1:连续触发场景下防抖与节流执行时机对比

二、防抖函数(Debounce)的代码实现

编写防抖函数时,除了维护基础定时器清空逻辑外,还需处理两个关键工程问题:

  1. 保持原函数的 this 与参数传递:确保事件对象 event 及其上下文不发生丢失。
  2. 支持首发立即执行(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;
}
注意事项:若在 Vue、React 等声明式框架中使用防抖,应避免在组件的每次渲染(Render)函数中直接调用 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 属性发起实际网络请求。

JavaScript性能优化实例:防抖节流与懒加载代码实现
图2:图片懒加载视口检测与核心机制对比

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();
}

五、技术选型决策与组合实战

在复杂的前端业务中,开发者往往面临“用防抖还是用节流”、“懒加载该选原生属性还是代码实现”的选择困惑。下图展示了高频事件与加载优化的选型决策逻辑:

JavaScript性能优化实例:防抖节流与懒加载代码实现
图3:高频事件与加载优化选型路径决策树

六、综合实战排坑与注意事项

在实际项目落地中,细节往往决定了优化的成败。以下梳理了三项技术在实战中最常见的陷阱与排查要点:

技术项 常见踩坑点 产生后果 正确应对策略
防抖 (Debounce) 组件重新渲染导致闭包重新生成 每次事件都拿到新函数,防抖完全失效 使用 useCallback 或模块级作用域固定闭包实例
节流 (Throttle) 漏掉 trailing: true 尾部处理 用户最后一次操作未得到执行,导致数据不一致 配置首尾支持,在间隔结束后执行最后一次入参调用
懒加载 (Lazy Load) 图片没有提前占据固定宽高或占位容器 图片加载后撑开高度,造成严重累积布局偏移(CLS) 设置 aspect-ratio 或骨架屏占位容器保持结构稳定
内存管理 页面或单页路由销毁未移除监听或解绑观察者 DOM 已从视图脱离但闭包常驻,引起内存泄漏 在组件销毁钩子中调用 .cancel() 或 unobserve()
JavaScript性能优化实例:防抖节流与懒加载代码实现
图4:JavaScript性能优化实施与避坑全景图

七、总结

防抖、节流与懒加载是前端性能优化中最经典也是最高效的三个实战手段。总结其核心规律:

  • 面对连续动作,若只需要在用户彻底停下后获取最终结果,采用防抖(Debounce);
  • 面对持续过程,若需要在过程中按一定节拍保持响应与刷新,采用节流(Throttle);
  • 面对大量长列表重媒体资源,优先采用现代 IntersectionObserver 进行异步懒加载,并配合预加载缓冲与尺寸占位,消除页面卡顿与抖动。

通过本文提供的可配置实现方案,开发者可以在实际项目中直接应用,并根据具体业务需求弹性调整延时、阈值与首尾触发逻辑,实现平滑、稳定、高效的用户交互体验。

作者最新文章

相关标签:

相关文章
更多