第16课:JavaScript性能优化——让代码飞起来

105 阅读2分钟

星光不问赶路人,时光不负有心人!

当你的网页变得卡顿、滚动不流畅、甚至频繁崩溃时,你是否想化身“代码医生”精准诊断问题?本节课将传授 防抖节流、虚拟滚动、内存管理 三大绝技,用「大型数据列表优化」实战让你的应用丝滑如德芙!

一、性能问题三大“元凶”

1. 过度重复操作

场景: 搜索框输入实时请求、窗口滚动频繁计算

解法: 防抖(debounce)与节流(throttle)

2. 庞大 DOM 操作

场景: 渲染千行表格、无限滚动加载

解法: 虚拟滚动(Virtual Scroll)

3. 内存泄漏

场景: 未清理的定时器、遗忘的事件监听、游离的 DOM 引用

解法: 及时回收资源

二、防抖与节流:给代码装上“刹车”

1. 防抖(Debounce):最后一人上电梯

function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 使用:输入停止300ms后触发搜索
input.addEventListener('input', debounce(search, 300));

2. 节流(Throttle):发车时间表

function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 使用:滚动时每200ms计算一次
window.addEventListener('scroll', throttle(calculatePosition, 200));

防抖: 等你一段时间内没有再触发动作,才会执行一次。

节流: 每隔固定时间就执行一次,不管你在那段时间内有没有触发动作。

三、虚拟滚动:渲染海量数据的“障眼法”

1. 核心原理

  • 可视区域渲染:只渲染用户能看到的 10~20 条数据
  • 动态计算位置:通过滚动位置推算显示内容

2. 实战代码(简化版)

<!DOCTYPE html>
<html>
<head>
  <style>
    #container {
      height: 600px;
      overflow-y: auto;
      border: 1px solid #ccc;
    }

    .item {
      height: 30px;
      line-height: 30px;
      padding: 0 10px;
      border-bottom: 1px solid #eee;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id="content"></div>
  </div>

  <script>
    // 生成模拟数据
    const data = Array.from({length: 100000}, (_, i) => `Item ${i + 1}`);

    // 获取容器元素
    const container = document.getElementById('container');
    const content = document.getElementById('content');

    // 配置参数
    const itemHeight = 30;      // 每个项目的高度
    const buffer = 10;           // 缓冲区项目数
    let visibleCount = 0;        // 可见区域项目数
    let startIndex = 0;          // 起始索引
    let endIndex = 0;            // 结束索引

    // 初始化容器
    function init() {
      // 计算可见区域可容纳项目数
      visibleCount = Math.ceil(container.clientHeight / itemHeight);

      // 设置内容容器总高度(撑开滚动条)
      content.style.height = `${data.length * itemHeight}px`;

      // 初始渲染
      updateList();

      // 监听滚动事件
      container.addEventListener('scroll', () => {
        requestAnimationFrame(updateList);
      });
    }

    // 更新列表
    function updateList() {
      // 计算新的索引(包含缓冲区)
      const scrollTop = container.scrollTop;
      const newStartIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
      const newEndIndex = Math.min(
        data.length,
        newStartIndex + visibleCount + buffer * 2
      );

      // 如果索引没有变化则不需要更新
      if (startIndex === newStartIndex && endIndex === newEndIndex) return;

      // 更新索引
      startIndex = newStartIndex;
      endIndex = newEndIndex;

      // 创建文档片段
      const fragment = document.createDocumentFragment();

      // 添加当前需要显示的项目
      for (let i = startIndex; i < endIndex; i++) {
        const item = document.createElement('div');
        item.className = 'item';
        item.textContent = data[i];
        fragment.appendChild(item);
      }

      // 更新DOM
      content.innerHTML = '';
      content.appendChild(fragment);

      // 调整内容容器的padding(保持滚动位置)
      content.style.paddingTop = `${startIndex * itemHeight}px`;
      content.style.paddingBottom = `${(data.length - endIndex) * itemHeight}px`;
    }

    // 初始化
    init();
  </script>
</body>
</html>

效果对比:

  • 传统渲染:100000 条 → 严重卡顿

  • 虚拟滚动:仅渲染可视区域 → 流畅滚动

四、内存泄漏排查与预防

1. 常见泄露场景

  • 未清除的 setInterval/setTimeout

  • 未解绑的事件监听器

  • 闭包中意外保留的变量引用

3. 预防技巧

// 定时器清理
const timer = setInterval(...);
// 组件销毁时
clearInterval(timer);

// 事件监听解绑
element.addEventListener('click', handler);
// 不再需要时
element.removeEventListener('click', handler);

// 清除DOM引用
const elements = document.querySelectorAll('.temp');
elements.forEach(el => el.remove());