性能优化专题

170 阅读6分钟

检测性能指标的手段 浏览器内置工具性能监控平台埋点上报

🔹 1. 浏览器内置工具

最直接的方式就是利用浏览器的工具来观测性能指标:

  • Chrome DevTools Performance

    • 能查看页面加载时间、脚本执行时间、渲染(Rendering)、回流重绘等。
    • 核心指标:渲染帧率 (FPS)脚本耗时长任务(Long Tasks >50ms)
  • Lighthouse(Chrome 自带)

    • 一键生成性能报告。

    • 常见指标:

      • FCP (First Contentful Paint) :首次内容绘制时间。
      • LCP (Largest Contentful Paint) :最大内容绘制时间,衡量主内容加载速度。
      • CLS (Cumulative Layout Shift) :累计布局偏移,衡量页面抖动。
      • TTI (Time to Interactive) :可交互时间。
      • TBT (Total Blocking Time) :阻塞主线程的时间。
  • Performance API(JS 原生 API)

    • performance.now():精确计时。

    • PerformanceObserver:可以监听 资源加载长任务FCP/LCP/CLS 等。

    • 例如:

      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => console.log(entry));
      });
      observer.observe({ entryTypes: ["paint", "largest-contentful-paint", "layout-shift"] });
      

🔹 2. 接入前端监控 / APM 平台

如果是线上项目,需要把指标收集上报,常见工具有:

  • 开源/第三方监控平台

    • Sentry(性能监控 + 错误监控)
    • Datadog、New Relic、阿里 ARMS、腾讯云前端监控
    • Google Analytics 4 (GA4) 也支持 Web Vitals 上报
  • Web Vitals 库(Google 提供)

    • 直接测量 FCP、LCP、CLS、FID、TTFB 等核心指标。

    • 用法:

      import { onCLS, onFID, onLCP } from 'web-vitals';
      
      onCLS(console.log);
      onFID(console.log);
      onLCP(console.log);
      
  • 自建监控系统

    • 使用 Performance API + 埋点,上报到日志/数据库。

    • 收集:

      • 白屏时间(从请求到首屏渲染)
      • 首次可交互时间
      • 资源加载耗时
      • 错误日志(JS 错误、Promise 未捕获异常、静态资源 404)

🔹 3. 用户体验层面的指标

除了技术上的 FPS、TTI,还可以通过埋点采集 真实用户体验(RUM, Real User Monitoring)

  • 页面打开耗时(首屏时间、白屏时间)
  • 路由切换耗时
  • 首次输入延迟(FID)
  • 滚动/动画卡顿率
  • API 请求耗时
  • 错误率(JS 错误、请求错误)

🔹 总结

项目性能指标观测的常见途径:

  1. 开发阶段:Chrome DevTools + Lighthouse
  2. 线上环境:Web Vitals + Performance API 埋点
  3. 大规模应用:接入 Sentry / Datadog / ARMS 等监控平台
  4. 自建方案:埋点收集 + 上报,做 Dashboard 分析

1. 减少重绘和回流

避免频繁操作 DOM: 合并多次操作为一次,或使用文档片段 (DocumentFragment)。 避免用 innerHTML 频繁修改大块内容。

避免触发同步回流属性: 避免在同一帧内读取和修改布局相关属性(如 offsetTop、clientHeight)。

用 CSS 替代 JS 动画: CSS 动画通过 transform 和 opacity 实现硬件加速,性能更优。

减少复杂选择器: 使用具体的类名,避免深层次嵌套选择器。

2. 使用虚拟 DOM 或虚拟列表

使用框架如 React 或 Vue,它们的虚拟 DOM 可优化大量 DOM 操作。 避免重新渲染整个组件树,使用 shouldComponentUpdate 或 React.memo 等优化渲染。

虚拟列表**** 当列表内容较多(如上千条)时,仅渲染可见区域的数据:

import { FixedSizeList as List } from 'react-window';
<List
  height={500}
  itemCount={1000}
  itemSize={35}
  width={300}
>
  {({ index, style }) => <div style={style}>Row {index}</div>}
</List>

3. 事件节流与防抖

节流 (Throttle) :控制函数调用的频率。

const throttle = (func, delay) => {
  let lastCall = 0;
  return (...args) => {
    const now = new Date().getTime();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
};
window.addEventListener('scroll', throttle(handleScroll, 100));

防抖 (Debounce) :推迟函数调用直到事件停止一段时间。

const debounce = (func, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => func(...args), delay);
  };
};
window.addEventListener('resize', debounce(handleResize, 300));

4. 懒加载 图片懒加载

<img src="image.jpg" loading="lazy" alt="Lazy Load Image" />

使用 Intersection Observer:

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});
document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img));

组件懒加载(如 React):

const LazyComponent = React.lazy(() => import('./Component'));
<React.Suspense fallback={<div>Loading...</div>}>
  <LazyComponent />
</React.Suspense>

5. 优化资源加载

压缩静态资源

使用工具如 Webpack、Vite 打包并压缩 CSS、JS。 压缩图片,使用现代格式(如 WebP)。

使用 CDN

把静态资源放到 CDN,减少服务器负载。

使用 HTTP/2

提高多资源加载效率。

Tree Shaking

移除未使用的代码模块:

"sideEffects": false

代码分包: 将大体积的模块按需加载,减少首屏体积。

6. CSS和JS优化****

CSS

减少未使用的 CSS 规则。 避免使用大面积的 box-shadow 或复杂的渐变。

JS

使用轻量库(如 lodash 的按需加载)。 避免阻塞主线程,使用 setTimeout 或 requestIdleCallback。

7. 优化渲染线程和任务队列****

使用 Web Worker

把计算密集型任务移到 Web Worker:

const worker = new Worker('worker.js');
worker.postMessage(data);
worker.onmessage = (e) => console.log(e.data);

减少长任务

避免阻塞主线程的任务,将任务切片:

const chunkProcess = (items) => {
  const processChunk = () => {
    const chunk = items.splice(0, 100); // 每次处理 100 个
    chunk.forEach(doSomething);
    if (items.length > 0) {
      requestAnimationFrame(processChunk);
    }
  };
  processChunk();
};

8. 首屏优化****

SSR / SSG:在服务端渲染首屏内容,减少白屏时间。

Critical CSS:提取关键 CSS,优先加载首屏样式。

Preload 和 Prefetch

<link rel="preload" href="styles.css" as="style">
<link rel="prefetch" href="next-page.js">

9. 硬件加速****

使用 CSS3 硬件加速:

transform: translate3d(0, 0, 0); /* 开启 GPU 加速 */
will-change: transform;

10. 逐帧渲染组件****

核心思想****

  1. 逐帧分批渲染:****

使用 requestAnimationFrame 逐帧加载部分内容,避免一次性渲染大量组件导致的性能问题。

  1. 条件渲染控制:****

根据组件的加载状态,逐步渲染子组件。

  1. 封装逻辑:****

创建一个通用的逐帧渲染组件,适用于不同场景。

import React, { useState, useEffect } from 'react';

interface ProgressiveRenderProps<T> {
  items: T[]; // 渲染的列表数据
  renderItem: (item: T, index: number) => React.ReactNode; // 渲染函数
  batchSize?: number; // 每帧渲染的组件数量
  delay?: number; // 每帧之间的间隔时间(ms),可选
}

const ProgressiveRender = <T,>({
  items,
  renderItem,
  batchSize = 10, // 默认每帧渲染 10 个
  delay = 0,      // 默认没有额外延迟
}: ProgressiveRenderProps<T>) => {
  const [renderCount, setRenderCount] = useState(0); // 当前渲染的项数

  useEffect(() => {
    let isMounted = true;

    const renderFrame = () => {
      if (!isMounted) return;

      setRenderCount((prev) => {
        const nextCount = Math.min(prev + batchSize, items.length);
        if (nextCount < items.length) {
          requestAnimationFrame(renderFrame); // 继续下一帧渲染
        }
        return nextCount;
      });
    };

    // 开始逐帧渲染
    const timeoutId = setTimeout(() => requestAnimationFrame(renderFrame), delay);

    // 清理逻辑
    return () => {
      isMounted = false;
      clearTimeout(timeoutId);
    };
  }, [items, batchSize, delay]);

  return (
    <>
      {items.slice(0, renderCount).map((item, index) => renderItem(item, index))}
    </>
  );
};

export default ProgressiveRender;

使用方法:

import React from 'react';
import ProgressiveRender from './ProgressiveRender';

const App = () => {
  const items = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);

  return (
    <div style={{ padding: '20px' }}>
      <h1>逐帧渲染示例</h1>
      <ProgressiveRender
        items={items}
        batchSize={20} // 每帧渲染 20 个组件
        delay={100}    // 每帧延迟 100ms
        renderItem={(item, index) => (
          <div
            key={index}
            style={{
              margin: '10px 0',
              padding: '10px',
              backgroundColor: '#f0f0f0',
              border: '1px solid #ddd',
            }}
          >
            {item}
          </div>
        )}
      />
    </div>
  );
};

export default App;

代码解析****

  1. ProgressiveRender 组件:****

• 接收 items(需要渲染的列表数据)。 • 每帧渲染一部分数据,默认批量渲染数量为 batchSize。 • 使用 requestAnimationFrame 来确保每帧在浏览器的下一次重绘时执行渲染。

  1. renderItem 函数:****

• 接收每个 item 和 index,用于渲染单个子组件。 • 渲染逻辑完全由使用者控制,ProgressiveRender 不关心子组件的具体内容。

  1. useEffect 中的逻辑:****

• 使用 requestAnimationFrame 分批渲染,调用 setRenderCount 更新渲染状态。 • 在组件销毁时停止渲染。

  1. 防止性能问题:****

• 通过 batchSize 和 delay 控制每帧渲染的内容量和间隔时间。