2026-01-19 10:21:18 +08:00

8.4 KiB
Raw Permalink Blame History

性能与优化

1. 图片懒加载

场景:电商商品列表、新闻资讯页、图片画廊、任何大量图片展示页面。 解决:首屏加载大量图片消耗带宽和性能,延迟加载可视区域外的图片。

IntersectionObserver 方案(推荐)

function lazyLoadImages() {
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {  // 图片进入可视区域
        const img = entry.target;
        img.src = img.dataset.src;  // 加载真实图片
        img.classList.remove('lazy');
        observer.unobserve(img);    // 加载完成后停止观察
      }
    });
  }, {
    rootMargin: '50px'  // 提前 50px 开始加载,优化体验
  });

  // 观察所有带 lazy 类的图片
  document.querySelectorAll('img.lazy').forEach(img => observer.observe(img));
}

// HTML: <img class="lazy" data-src="real-image.jpg" src="placeholder.jpg" />

传统滚动监听方案

function lazyLoadScroll() {
  const lazyImages = document.querySelectorAll('img.lazy');
  
  // 节流处理,避免频繁触发
  const loadImage = throttle(() => {
    lazyImages.forEach(img => {
      // 图片顶部进入视口下方 100px 范围内
      if (img.getBoundingClientRect().top < window.innerHeight + 100) {
        img.src = img.dataset.src;
        img.classList.remove('lazy');
      }
    });
  }, 200);  // 每 200ms 最多执行一次

  window.addEventListener('scroll', loadImage);
  loadImage();  // 初始加载可见的图片
}

2. 虚拟列表(大数据渲染优化)

场景:聊天记录、日志列表、表格万级数据、任何长列表滚动场景。 解决:渲染万级 DOM 节点卡顿,只渲染可视区域的元素,大幅提升性能。

class VirtualList {
  constructor({ container, itemHeight, totalItems, renderItem }) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.totalItems = totalItems;
    this.renderItem = renderItem;
    
    this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;
    this.startIndex = 0;
    
    this.init();
  }
  
  init() {
    // 创建容器结构
    this.container.style.overflow = 'auto';
    this.container.style.position = 'relative';
    
    // 占位元素,撑开滚动高度
    this.phantom = document.createElement('div');
    this.phantom.style.height = `${this.totalItems * this.itemHeight}px`;
    
    // 实际渲染的列表容器
    this.content = document.createElement('div');
    this.content.style.position = 'absolute';
    this.content.style.top = '0';
    this.content.style.width = '100%';
    
    this.container.appendChild(this.phantom);
    this.container.appendChild(this.content);
    
    this.container.addEventListener('scroll', throttle(() => this.onScroll(), 16));
    this.render();
  }
  
  onScroll() {
    const scrollTop = this.container.scrollTop;
    this.startIndex = Math.floor(scrollTop / this.itemHeight);
    this.content.style.transform = `translateY(${this.startIndex * this.itemHeight}px)`;
    this.render();
  }
  
  render() {
    const fragment = document.createDocumentFragment();
    const endIndex = Math.min(this.startIndex + this.visibleCount, this.totalItems);
    
    for (let i = this.startIndex; i < endIndex; i++) {
      const item = this.renderItem(i);
      item.style.height = `${this.itemHeight}px`;
      fragment.appendChild(item);
    }
    
    this.content.innerHTML = '';
    this.content.appendChild(fragment);
  }
}

// 使用示例
new VirtualList({
  container: document.getElementById('list'),
  itemHeight: 50,
  totalItems: 10000,
  renderItem: (index) => {
    const div = document.createElement('div');
    div.textContent = `Item ${index}`;
    return div;
  }
});

3. 函数节流在滚动事件中的应用

场景:吸顶效果、返回顶部按钮显隐、数据埋点、滚动位置计算。 解决:滚动事件每秒触发数十次,频繁执行回调导致页面卡顿。

// 节流函数:确保最后一次触发也会执行
function throttle(fn, delay) {
  let lastTime = 0;   // 上次执行时间
  let timer = null;   // 定时器 ID
  
  return function(...args) {
    const now = Date.now();
    const remaining = delay - (now - lastTime);  // 剩余等待时间
    
    if (remaining <= 0) {
      // 已超过等待时间,立即执行
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastTime = now;
      fn.apply(this, args);
    } else if (!timer) {
      // 未到时间且没有定时器,设置定时器保证最后一次执行
      timer = setTimeout(() => {
        lastTime = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

// 应用场景示例
window.addEventListener('scroll', throttle(() => {
  console.log('滚动位置:', window.scrollY);
  // 吸顶效果、返回顶部按钮显示等
}, 100));  // 每 100ms 最多执行一次

window.addEventListener('resize', throttle(() => {
  console.log('窗口大小:', window.innerWidth, window.innerHeight);
}, 200));

4. requestAnimationFrame 优化动画

场景平滑滚动、CSS 动画替代方案、Canvas/WebGL 渲染循环、游戏开发。 解决setTimeout/setInterval 动画帧率不稳定rAF 与屏幕刷新率同步,动画更流畅。

// 使用 requestAnimationFrame 实现平滑滚动
function smoothScrollTo(target, duration = 500) {
  const start = window.scrollY;        // 起始位置
  const distance = target - start;     // 滚动距离
  const startTime = performance.now(); // 高精度起始时间
  
  function step(currentTime) {
    const elapsed = currentTime - startTime;  // 已经过的时间
    const progress = Math.min(elapsed / duration, 1);  // 进度 0-1
    
    // 缓动函数easeOutCubic动画先快后慢
    const easeProgress = 1 - Math.pow(1 - progress, 3);
    window.scrollTo(0, start + distance * easeProgress);
    
    if (progress < 1) {
      requestAnimationFrame(step);  // 继续下一帧
    }
  }
  
  requestAnimationFrame(step);  // 开始动画
}

// rAF 节流:与屏幕刷新率同步
function rafThrottle(fn) {
  let ticking = false;  // 是否已请求下一帧
  return function(...args) {
    if (!ticking) {
      requestAnimationFrame(() => {
        fn.apply(this, args);
        ticking = false;
      });
      ticking = true;
    }
  };
}

5. Web Worker 处理耗时任务

场景大文件解析Excel/CSV、图像处理、复杂计算加密/压缩)、数据分析。 解决耗时JS计算阻塞主线程导致页面卡顿Worker 在后台线程执行不影响UI。

// 主线程 (main.js)
const worker = new Worker('worker.js');  // 创建后台线程

// 发送数据给 Worker
worker.postMessage({ type: 'heavyTask', data: largeArray });

// 接收 Worker 返回的结果
worker.onmessage = (e) => {
  console.log('处理结果:', e.data);
};

// Worker 线程 (worker.js)
self.onmessage = (e) => {
  if (e.data.type === 'heavyTask') {
    // 在后台线程执行耗时计算,不影响主线程 UI
    const result = e.data.data.map(item => /* 耗时计算 */);
    self.postMessage(result);  // 返回结果给主线程
  }
};

6. 时间切片(避免长任务阻塞)

场景React Fiber 架构原理、大量 DOM 初始化、批量数据处理不卡顿UI。 解决长任务占据主线程超过50ms导致用户交互无响应分片执行保证响应性。

// 时间切片:将长任务分成小块执行
async function timeSlice(tasks, chunkSize = 5) {
  const results = [];
  
  for (let i = 0; i < tasks.length; i += chunkSize) {
    const chunk = tasks.slice(i, i + chunkSize);
    
    // 处理一批任务
    for (const task of chunk) {
      results.push(task());
    }
    
    // 让出主线程,让浏览器有机会处理用户交互
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  
  return results;
}

// 使用 requestIdleCallback在浏览器空闲时执行
function processInIdle(tasks) {
  let index = 0;
  
  function work(deadline) {
    // 当前帧还有剩余时间时执行任务
    while (index < tasks.length && deadline.timeRemaining() > 0) {
      tasks[index++]();
    }
    // 如果还有任务,等待下次空闲时继续
    if (index < tasks.length) {
      requestIdleCallback(work);
    }
  }
  
  requestIdleCallback(work);  // 开始执行
}