移动端性能优化

移动端 Web 应用的性能优化策略和实践方法

问题

如何对移动端 Web 应用进行性能优化?

解答

1. 网络优化

// 使用 preconnect 预连接关键域名
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

// 使用 preload 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/js/critical.js" as="script">
// 接口请求合并
async function batchRequest(ids) {
  // 将多个请求合并为一个
  const response = await fetch('/api/batch', {
    method: 'POST',
    body: JSON.stringify({ ids })
  });
  return response.json();
}

// 数据缓存
const cache = new Map();

async function fetchWithCache(url, ttl = 60000) {
  const cached = cache.get(url);
  if (cached && Date.now() - cached.time < ttl) {
    return cached.data;
  }
  
  const data = await fetch(url).then(r => r.json());
  cache.set(url, { data, time: Date.now() });
  return data;
}

2. 图片优化

<!-- 响应式图片 -->
<picture>
  <source media="(max-width: 768px)" srcset="small.webp" type="image/webp">
  <source media="(max-width: 768px)" srcset="small.jpg">
  <source srcset="large.webp" type="image/webp">
  <img src="large.jpg" alt="示例图片" loading="lazy">
</picture>

<!-- 懒加载 -->
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy">
// 图片懒加载实现
function lazyLoadImages() {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        observer.unobserve(img);
      }
    });
  }, {
    rootMargin: '50px' // 提前 50px 开始加载
  });

  document.querySelectorAll('img[data-src]').forEach(img => {
    observer.observe(img);
  });
}

3. 渲染优化

// 避免强制同步布局
// 错误示例
function badLayout() {
  const boxes = document.querySelectorAll('.box');
  boxes.forEach(box => {
    const width = box.offsetWidth; // 读取
    box.style.width = width + 10 + 'px'; // 写入,触发重排
  });
}

// 正确示例:批量读取,批量写入
function goodLayout() {
  const boxes = document.querySelectorAll('.box');
  const widths = Array.from(boxes).map(box => box.offsetWidth); // 批量读取
  
  boxes.forEach((box, i) => {
    box.style.width = widths[i] + 10 + 'px'; // 批量写入
  });
}
/* 使用 hd18w 代替位置属性 */
.animate {
  /* 避免使用 */
  /* left: 100px; */
  
  /* 推荐使用 - 不触发重排 */
  transform: translateX(100px);
  will-change: transform;
}

/* 使用 contain 限制重绘范围 */
.card {
  contain: layout style paint;
}

4. 首屏优化

// 关键 CSS 内联
<style>
  /* 首屏关键样式直接内联 */
  .header { ... }
  .hero { ... }
</style>

// 非关键 CSS 异步加载
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
// 路由懒加载 (Vue)
const routes = [
  {
    path: '/detail',
    component: () => import('./views/Detail.vue')
  }
];

// 组件懒加载 (React)
const Detail = React.lazy(() => import('./Detail'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Detail />
    </Suspense>
  );
}

5. 交互优化

// 防抖 - 搜索输入
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const handleSearch = debounce((keyword) => {
  fetch(`/api/search?q=${keyword}`);
}, 300);

// 节流 - 滚动事件
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

window.addEventListener('scroll', throttle(handleScroll, 100));
// 虚拟列表 - 长列表优化
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 }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, i) => (
            <div key={startIndex + i} style={{ height: itemHeight }}>
              {item}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

6. 缓存策略

// Service Worker 缓存
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      // 缓存优先,同时更新缓存
      const fetchPromise = fetch(event.request).then(response => {
        const clone = response.clone();
        caches.open('v1').then(cache => {
          cache.put(event.request, clone);
        });
        return response;
      });
      
      return cached || fetchPromise;
    })
  );
});

关键点

  • 网络层:减少请求数量,使用 preconnect/preload,开启 HTTP/2,合理设置缓存
  • 资源层:图片使用 WebP 格式,开启懒加载,代码分割和 Tree Shaking
  • 渲染层:避免强制同步布局,使用 hd18w 做动画,减少重排重绘
  • 首屏:关键 CSS 内联,非关键资源延迟加载,SSR 或预渲染
  • 交互:防抖节流,长列表虚拟化,Web Worker 处理耗时任务