其他优化方案

前端性能优化的其他实用方案

问题

除了常见的懒加载、代码分割外,还有哪些前端优化方案?

解答

1. 资源预加载

<!-- 预连接:提前建立连接 -->
<link rel="preconnect" href="https://api.example.com">

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">

<!-- 预加载:提前加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/critical.css" as="style">

<!-- 预获取:空闲时加载下一页资源 -->
<link rel="prefetch" href="/next-page.js">

2. Web Workers 处理耗时任务

// worker.js
self.onmessage = function(e) {
  const { data } = e;
  // 执行耗时计算
  const result = heavyComputation(data);
  self.postMessage(result);
};

function heavyComputation(data) {
  // 模拟复杂计算
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum += data[i] * Math.sqrt(data[i]);
  }
  return sum;
}

// main.js
const worker = new Worker('worker.js');

worker.postMessage(largeArray);

worker.onmessage = function(e) {
  console.log('计算结果:', e.data);
};

3. 请求优化

// 请求合并
class RequestBatcher {
  constructor(batchFn, delay = 50) {
    this.queue = [];
    this.batchFn = batchFn;
    this.delay = delay;
    this.timer = null;
  }

  add(id) {
    return new Promise((resolve, reject) => {
      this.queue.push({ id, resolve, reject });
      
      if (!this.timer) {
        this.timer = setTimeout(() => this.flush(), this.delay);
      }
    });
  }

  async flush() {
    const batch = this.queue;
    this.queue = [];
    this.timer = null;

    const ids = batch.map(item => item.id);
    try {
      // 批量请求
      const results = await this.batchFn(ids);
      batch.forEach((item, i) => item.resolve(results[i]));
    } catch (err) {
      batch.forEach(item => item.reject(err));
    }
  }
}

// 使用
const userBatcher = new RequestBatcher(async (ids) => {
  const res = await fetch(`/api/users?ids=${ids.join(',')}`);
  return res.json();
});

// 多次调用会合并成一次请求
userBatcher.add(1);
userBatcher.add(2);
userBatcher.add(3);

4. 虚拟列表

function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);

  // 计算可见范围
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  );

  // 只渲染可见项
  const visibleItems = items.slice(startIndex, endIndex);
  const offsetY = startIndex * itemHeight;

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, i) => (
            <div key={startIndex + i} style={{ height: itemHeight }}>
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

5. 骨架屏

/* 骨架屏动画 */
.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
function SkeletonCard() {
  return (
    <div className="card">
      <div className="skeleton" style={{ width: 60, height: 60, borderRadius: '50%' }} />
      <div className="skeleton" style={{ width: '80%', height: 20, marginTop: 12 }} />
      <div className="skeleton" style={{ width: '60%', height: 16, marginTop: 8 }} />
    </div>
  );
}

6. 字体优化

/* 字体显示策略 */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* 先用系统字体,加载完再替换 */
}

/* 字体子集化:只加载需要的字符 */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom-subset.woff2') format('woff2');
  unicode-range: U+0000-00FF; /* 只包含基本拉丁字符 */
}

7. 避免布局抖动

// 错误:读写交替导致强制同步布局
elements.forEach(el => {
  const width = el.offsetWidth; // 读
  el.style.width = width + 10 + 'px'; // 写
});

// 正确:先批量读,再批量写
const widths = elements.map(el => el.offsetWidth); // 批量读
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px'; // 批量写
});

// 使用 requestAnimationFrame 分离读写
function optimizedUpdate() {
  // 读取阶段
  const measurements = elements.map(el => el.getBoundingClientRect());
  
  // 写入阶段
  requestAnimationFrame(() => {
    elements.forEach((el, i) => {
      el.style.transform = `translateX(${measurements[i].width}px)`;
    });
  });
}

关键点

  • preload/prefetch:关键资源预加载,空闲时预获取下一页
  • Web Workers:将耗时计算移出主线程,避免阻塞 UI
  • 请求合并:短时间内多次请求合并为一次,减少网络开销
  • 虚拟列表:只渲染可视区域,解决长列表性能问题
  • 避免布局抖动:批量读写 DOM,使用 hd18w 代替位置属性