面试必考+实战必备:彻底掌握防抖与节流

60 阅读6分钟

从性能优化角度深入理解防抖与节流:告别无效重绘和回流

引言:为什么我们需要性能优化?

在日常开发和使用各种App时,你可能没有意识到,我们一直在与防抖和节流这两个概念打交道。比如:

  • 在搜索框输入关键词时,浏览器不会每输入一个字就立即请求服务器
  • 滚动页面时,不会每移动一个像素就重新渲染整个页面

如果没有这些优化措施,每次用户的细微操作都会触发昂贵的计算和网络请求,对于千万级用户的App来说,服务器很快就会崩溃。今天,我们就来深入探讨这两种性能优化技术。

一、性能问题的本质:重绘与回流

在理解防抖和节流之前,我们需要先了解浏览器渲染机制中的两个关键概念:

1. 回流(Reflow)

当元素的尺寸、位置或布局发生变化时,浏览器需要重新计算元素的位置和几何属性,这个过程称为回流。回流是性能开销最大的操作

2. 重绘(Repaint)

当元素的外观(如颜色、背景等)发生变化,但不影响布局时,浏览器只需要重新绘制受影响的部分,这个过程称为重绘。

重要事实:回流一定会触发重绘,但重绘不一定会触发回流。

二、未优化的性能噩梦

让我们先看一个没有进行任何性能优化的示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>未优化的性能示例</title>
</head>
<body>
    <div>
        <input type="text" id="undebounce" value="无性能优化"/><br>
    </div>
    <script>
        const inputa = document.getElementById('undebounce');
        
        // 模拟Ajax请求
        function ajax(content){
            console.log('ajax request',content);
            // 实际开发中这里可能是网络请求
            // 每次请求都会导致浏览器重新计算和渲染
        }

        // 频繁触发:按键抬起时触发
        inputa.addEventListener('keyup',function(e){
            ajax(e.target.value);
        });
    </script>
</body>
</html>

问题分析:

每输入一个字符,都会触发一次keyup事件,导致:

  1. 执行Ajax请求(网络I/O)
  2. 可能更新DOM(触发回流/重绘)
  3. 控制台输出(I/O操作)

效果演示:

屏幕录制 2025-12-30 215101.gif

性能代价:

  • 用户输入"hello"(5个字符) → 触发5次请求
  • 快速输入时可能触发数十次不必要的请求
  • 对于复杂操作,每次都可能触发页面回流

三、防抖(Debounce):"等待你说完"

防抖的核心思想:在事件被触发后,等待一段时间再执行回调。如果在这段时间内事件又被触发,则重新计时。

应用场景:

  1. 搜索框输入建议
  2. 窗口大小调整
  3. 表单验证

防抖实现原理:

<input type="text" id="debounce" value="防抖处理:"/>
<script>
const inputb = document.getElementById('debounce');

// 防抖函数
function debounce(fn, delay){
    // 使用闭包保存定时器ID
    let timerId;
    
    // 返回一个新函数
    return function(...args){
        const context = this;
        
        // 如果已有定时器,清除它(重新计时)
        if(timerId) clearTimeout(timerId);
        
        // 设置新的定时器
        timerId = setTimeout(function(){
            fn.apply(context, args);
        }, delay);
    }
}

// 创建防抖版本的Ajax函数
let debounceAjax = debounce(ajax, 500);

// 绑定事件
inputb.addEventListener('keyup', function(e){
    debounceAjax(e.target.value);
});
</script>

工作原理图解:

用户输入: h  e  l  l  o
触发时间: |  |  |  |  |
定时器:   重置 重置 重置 重置 执行
         ↑   ↑   ↑   ↑   ↑
        每次输入都重置500ms计时器
最终只在最后一次输入500ms后执行一次

实际效果:

屏幕录制 2025-12-30 220607.gif

性能提升:

  • 用户连续输入"hello" → 只触发1次请求
  • 减少网络请求约80%
  • 降低浏览器渲染压力

四、节流(Throttle):"定时执行"

节流的核心思想:在一定时间间隔内,只执行一次回调函数。

应用场景:

  1. 页面滚动事件
  2. 鼠标移动事件
  3. 游戏中的按键处理

节流实现原理:

<input type="text" id="throttle" value="节流处理:"/>
<script>
const inputc = document.getElementById('throttle');

// 节流函数 - 时间戳版本
function throttle(fn, delay){
    let lastTime = 0;  // 上次执行时间
    
    return function(...args){
        const context = this;
        const now = Date.now();  // 当前时间
        
        // 如果距离上次执行时间超过delay,则执行
        if(now - lastTime >= delay){
            fn.apply(context, args);
            lastTime = now;  // 更新上次执行时间
        }
    }
}

// 更完善的节流函数(支持最后一次执行)
function throttleAdvanced(fn, delay){
    let timerId = null;
    let lastTime = 0;
    
    return function(...args){
        const context = this;
        const now = Date.now();
        const remaining = delay - (now - lastTime);
        
        if(remaining <= 0){
            // 时间间隔已到,立即执行
            if(timerId){
                clearTimeout(timerId);
                timerId = null;
            }
            fn.apply(context, args);
            lastTime = now;
        } else if(!timerId){
            // 设置定时器,确保最后一次触发能执行
            timerId = setTimeout(() => {
                fn.apply(context, args);
                lastTime = Date.now();
                timerId = null;
            }, remaining);
        }
    }
}

let throttleAjax = throttleAdvanced(ajax, 300);

inputc.addEventListener('keyup', function(e){
    throttleAjax(e.target.value);
});
</script>

工作原理图解:

用户输入: h  e  l  l  o  w  o  r  l  d
触发时间: |  |  |  |  |  |  |  |  |  |
节流执行: ✓     ✓     ✓     ✓     ✓
时间间隔: ├──300ms──┤├──300ms──┤
          固定间隔执行,不管触发多少次

实际效果:

屏幕录制 2025-12-30 222323.gif

  • 可以看到,一秒一秒的输出

五、防抖 vs 节流:如何选择?

特性防抖 (Debounce)节流 (Throttle)
核心思想等待停止触发后再执行固定频率执行
执行时机最后一次触发后延迟执行按照固定时间间隔执行
适用场景搜索建议、窗口调整滚动事件、鼠标移动
用户体验等待用户"说完"保持响应但不频繁
执行次数1次(一组操作)多次(但有限制)

实际选择建议:

  1. 使用防抖当:

    • 只需关心最终状态
    • 连续操作只需响应一次
    • 例如:搜索框输入
  2. 使用节流当:

    • 需要保持操作的流畅性
    • 需要定期更新状态
    • 例如:无限滚动加载

六、现代开发中的实践

1. 使用Lodash等工具库

// 使用Lodash
import { debounce, throttle } from 'lodash';

const debouncedSearch = debounce(searchFunction, 300);
const throttledScroll = throttle(scrollFunction, 100);

2. React Hooks中的防抖节流

import { useCallback, useRef } from 'react';

function SearchComponent() {
    const debounceRef = useRef(null);
    
    const handleSearch = useCallback((value) => {
        if(debounceRef.current) {
            clearTimeout(debounceRef.current);
        }
        
        debounceRef.current = setTimeout(() => {
            // 执行搜索
            console.log('搜索:', value);
        }, 300);
    }, []);
    
    return <input onChange={(e) => handleSearch(e.target.value)} />;
}

3. Vue中的防抖节流

// 使用vue-debounce插件
<template>
    <input v-debounce:300ms="onSearch" />
</template>

<script>
export default {
    methods: {
        onSearch(value) {
            // 防抖处理后的搜索
        }
    }
}
</script>

七、性能对比实验

让我们创建一个简单的性能测试:

// 性能测试函数
function performanceTest(type, eventCount) {
    console.time(type);
    
    let count = 0;
    const testFunction = () => { count++; };
    
    let optimizedFunction;
    if(type === 'debounce') {
        optimizedFunction = debounce(testFunction, 100);
    } else if(type === 'throttle') {
        optimizedFunction = throttle(testFunction, 100);
    } else {
        optimizedFunction = testFunction;
    }
    
    // 模拟快速触发事件
    for(let i = 0; i < eventCount; i++) {
        optimizedFunction();
    }
    
    console.timeEnd(type);
    console.log(`${type} 执行次数:`, count);
}

// 测试
performanceTest('normal', 1000);    // 未优化
performanceTest('debounce', 1000);  // 防抖
performanceTest('throttle', 1000);  // 节流

八、最佳实践总结

  1. 理解业务场景:根据需求选择防抖或节流
  2. 合理设置时间间隔
    • 防抖:通常300-500ms
    • 节流:通常16.7ms(60fps)或100ms
  3. 考虑用户体验:不要过度优化导致响应迟钝
  4. 测试不同设备:移动端和桌面端的性能表现不同
  5. 监控实际性能:使用Chrome DevTools等工具监控

结语

防抖和节流是前端性能优化的基础但至关重要的技术。它们通过减少不必要的函数执行,有效降低了:

  • 网络请求次数
  • 浏览器回流/重绘
  • CPU和内存使用
  • 服务器压力

在当今追求极致用户体验的时代,合理使用这些优化技术,不仅能提升应用性能,还能显著改善用户体验。记住:优秀的性能优化是看不见的,但用户能感受到流畅与卡顿的差异。