CC 咖啡猫的工作空间 Coding Space

性能优化

性能指标体系

RAIL 模型(Google)

维度 含义 目标 获取方式
Response 响应用户输入 点击/触摸后 < 100ms 给出视觉反馈 Performance Observer
Animation 动画帧 每帧 < 16ms(60fps) requestAnimationFrame 回调间隔
Idle 空闲时段 将延迟任务分散到空闲时间片(< 50ms/块) requestIdleCallback
Load 加载完成 首次加载 < 5s(3G 慢速),后续加载 < 2s Navigation Timing API

Core Web Vitals(核心网页指标)

指标 全称 含义 良好 一般 获取方式
LCP Largest Contentful Paint 最大内容绘制(可见区最大元素渲染时间) < 2.5s 2.5s~4.0s > 4.0s PerformanceObserver largest-contentful-paint
INP Interaction to Next Paint 交互到下一次绘制的延迟 — 取代 FID < 200ms 200ms~500ms > 500ms PerformanceObserver first-input(实验)
CLS Cumulative Layout Shift 累计布局偏移 — 衡量视觉稳定性 < 0.1 0.1~0.25 > 0.25 PerformanceObserver layout-shift
// 收集 Core Web Vitals
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'largest-contentful-paint') {
      console.log('LCP:', entry.startTime);
    }
    if (entry.entryType === 'layout-shift') {
      console.log('CLS:', entry.value);
    }
  }
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.observe({ type: 'layout-shift', buffered: true });

其他关键指标

指标 含义 良好阈值
FCP First Contentful Paint — 首次内容绘制 < 1.8s
TTFB Time to First Byte — 首字节时间(服务端响应 + 网络延迟) < 800ms
TBT Total Blocking Time — FCP 到 TTI 之间所有长任务阻塞时间总和 < 200ms
SI Speed Index — 页面内容可见填充速度(Lighthouse) < 3.4s
TTI Time to Interactive — 可交互时间 < 3.8s
  • Lighthouse: 通过 Chrome DevTools / CLI lighthouse <url> 生成报告,评分基于加权组合
  • Performance API: performance.timing(已弃用)→ performance.getEntriesByType('navigation')(Navigation Timing Level 2)
  • web-vitals 库: npm install web-vitals — 封装好的 onLCP() onINP() onCLS() onFCP() onTTFB()

网络优化

HTTP 缓存策略

                 ┌─────────────┐
 请求 ──────────►│  强缓存      │◄── Cache-Control / Expires
                 └──────┬──────┘
                        │ 未命中
                        ▼
                 ┌─────────────┐
                 │  协商缓存    │◄── ETag / Last-Modified
                 └──────┬──────┘
                        │ 未修改
                        ▼
                  304 Not Modified
场景 Cache-Control 说明
静态资源(JS/CSS/图片) public, max-age=31536000, immutable 配合 content hash 文件名实现永久缓存
HTML no-cacheno-store 不缓存或每次校验
API 响应 private, max-age=60 仅允许浏览器缓存,不允许 CDN
# 静态资源推荐
Cache-Control: public, max-age=31536000, immutable
# HTML
Cache-Control: no-cache
# API 带认证
Cache-Control: private, max-age=60

压缩

算法 压缩比(文本) 压缩速度 解压速度 浏览器支持
Gzip ~70% 所有浏览器
Brotli ~80% 慢(需要更多 CPU) 快(解压接近 gzip) 现代浏览器(HTTPS 下)
  • Brotli 比 Gzip 小 15~25%,优先在 HTTPS 中使用
  • Nginx 配置 brotli on; + brotli_static on; 使用预压缩 .br 文件

CDN 策略

  • 预热: 大版本发布前主动请求所有静态资源 URL,拉回到边缘节点缓存
  • 刷新: 资源回退或紧急更新时清除 CDN 缓存,配合 ?v= 参数或 hash
  • 多级缓存: CDN Edge → CDN L1/L2 → Origin,减少回源

资源预加载

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

<!-- 预连接(包含 DNS + TCP + TLS) -->
<link rel="preconnect" href="https://fonts.googleapis.com">

<!-- 预加载关键资源(高优先级,当前页需要) -->
<link rel="preload" href="critical-font.woff2" as="font" crossorigin>

<!-- 预获取(低优先级,下一页可能用) -->
<link rel="prefetch" href="/next-page.js">

<!-- 预渲染(更高成本,渲染整个页面) -->
<link rel="prerender" href="/next-page.html">
类型 优先级 适用场景 是否当前页
preload 高(as 指定类型决定) 关键字体、首屏图片、关键 CSS 当前页
preconnect 第三方域名提前建立连接 当前页
prefetch 下面几个页面的资源 未来页
dns-prefetch 最低 只需 DNS 查询的域名 当前/未来

HTTP 升级

版本 核心改进 状态
HTTP/1.1 队头阻塞(HOL)、连接数限制(浏览器通常 6 个/域名) 逐步淘汰
HTTP/2 多路复用(单一 TCP 连接传输多个流)、HPACK 头部压缩、Server Push 主流,Server Push 已废弃(Chrome 106 移除,因为推送资源命中率低,不如 preload)
HTTP/3 QUIC 基于 UDP 实现、0-RTT 握手、无队头阻塞(丢包不影响其他流)、连接迁移 逐步普及

Server Push 废弃原因: 推送的资源即使浏览器已有缓存也会被推过来,浪费带宽;且 Service Worker 无法拦截;不如 preload + 103 Early Hints

雪碧图 / Base64 / SVG Sprite 对比

方案 适用场景 优点 缺点
CSS Sprite(雪碧图) 大量小图标(< 5KB 级别) 减少 HTTP 请求 维护困难、缩放模糊
Base64 极小图片(< 2KB),内联在 CSS 中 无额外请求 体积增大 33%、CSS 文件膨胀、不缓存
SVG Sprite 矢量图标 清晰、可缩放、可 CSS 控制颜色 复杂图形无法使用

资源优化

图片优化

格式选择

格式 压缩率 透明 动画 浏览器支持
JPEG 基准 全部
PNG 无损 全部
WebP 比 JPEG 小 25~35% 95%+
AVIF 比 JPEG 小 50%+ ~90%(Safari 16+)

策略: 优先 AVIF,WebP 兜底,JPEG 作为最后兼容

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="fallback" loading="lazy" decoding="async">
</picture>

响应式图片

<img
  srcset="
    image-320w.jpg 320w,
    image-640w.jpg 640w,
    image-1280w.jpg 1280w
  "
  sizes="(max-width: 640px) 100vw, (max-width: 1200px) 50vw, 33vw"
  src="image-1280w.jpg"
  alt="responsive"
>
  • srcsetw 描述符告诉浏览器图片真实宽度
  • sizes 告知浏览器在不同视口下图片的显示宽度
  • 浏览器根据 DPR + 视口宽度 + sizes 自动选择最合适的资源

Lazy Loading

<!-- 浏览器原生懒加载 -->
<img src="..." loading="lazy" decoding="async">

<!-- Intersection Observer 高级控制 -->
<img class="lazy" data-src="real.jpg" alt="">
<script>
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.src = entry.target.dataset.src;
        observer.unobserve(entry.target);
      }
    });
  });
  document.querySelectorAll('img.lazy').forEach(img => observer.observe(img));
</script>
  • loading="lazy": 浏览器处理,简单但无法精确控制
  • IntersectionObserver: 更灵活,可设置 rootMargin 提前加载

字体优化

策略 实现 效果
font-display: swap 字体加载期间先显示 fallback 字体 避免 FOIT(Flash of Invisible Text),但可能 FOUT
font-display: optional 字体如果在 100ms 内没加载完成,使用 fallback 弱网下不阻塞渲染
字体子集化 unicode-range + 工具(glyphhanger)只加载用到的字符 中文字体可从数 MB 降到几十 KB
WOFF2 优于 WOFF 的压缩格式 压缩比最大
preload 关键字体 <link rel="preload" as="font"> 尽早开始字体下载
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: swap;
  unicode-range: U+4E00-9FFF; /* 仅加载 CJK 字符 */
}

JS Bundle 优化

Tree Shaking

  • 依赖 ES Module 静态结构(import/export 而非 require
  • sideEffects: falsepackage.json 中标记无副作用
  • Webpack/Rollup 在 production 模式自动 tree-shake
// package.json
{
  "sideEffects": [
    "*.css",
    "*.global.js"
  ]
}

Code Splitting

┌──────────────────────┐
│   入口 main.js        │
├──────────────────────┤
│ Vendor (react, vue…)  │ ── splitChunks
├──────────────────────┤
│ 路由级 chunk           │ ── dynamic import
│ - /home -> Home.js    │
│ - /about -> About.js  │
│ - /detail -> Detail.js│
└──────────────────────┘
// React 动态导入
const Home = React.lazy(() => import('./pages/Home'));
// Vue 动态导入
const Home = () => import('./pages/Home.vue');
// Webpack splitChunks 配置
splitChunks: {
  chunks: 'all',
  cacheGroups: {
    vendor: {
      test: /[\\/]node_modules[\\/]/,
      name: 'vendors',
      chunks: 'all',
    },
  },
},

构建配置优化(Webpack 5)

特性 说明
splitChunks 提取公共代码到独立 chunk
externals 将 React/Vue 等大库标记为外部依赖(通过 CDN 加载)
DLL(已废弃) Webpack 5 已移除 DLLPlugin,改用 splitChunks + persistent caching

编译加速

工具 语言 替代场景 快多少
esbuild Go Babel / Terser / TS 编译 10~100x
SWC Rust Babel / Terser 20x+
Turbopack Rust Webpack(Next.js 集成) 实验性
// esbuild-loader(Webpack + esbuild)
{
  test: /\.ts$/,
  loader: 'esbuild-loader',
  options: { loader: 'tsx', target: 'es2020' }
}

依赖优化

优化 说明
webpack-bundle-analyzer 分析 bundle 组成,定位体积大的依赖
lodash-es 按需导入,支持 tree-shaking(import { debounce } from 'lodash-es'
moment → dayjs dayjs 仅 2KB,API 兼容 moment
date-fns 函数式日期工具,按需引入,tree-shakable
移除 polyfill core-js 按需引入,避免全部打包

渲染性能

关键渲染路径(Critical Rendering Path)

HTML ──► DOM
           │
CSS ────► CSSOM ──► Render Tree ──► Layout ──► Paint ──► Composite
           │          │               │          │
       解析 CSS   合并 DOM+CSSOM    计算布局   绘制像素   合并图层

优化内联关键 CSS:

<!-- 将首屏关键 CSS 内联在 <head> 中 -->
<style>
  /* 首屏样式,直接内联 */
  .header { position: fixed; top: 0; ... }
  .hero { height: 100vh; ... }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

重排(Reflow)与重绘(Repaint)

操作 触发 成本
重排 (Reflow/Layout) 改变元素几何属性(width/height/padding/margin/position) 高 — 涉及 Layout → Paint → Composite
重绘 (Repaint) 改变视觉但不影响布局(color/background/visibility) 中 — 涉及 Paint → Composite
合成 (Composite) 仅改变 transform/opacity 低 — 只涉及 Composite

最佳实践

// ❌ 频繁触发重排
el.style.width = '100px';
el.style.height = '200px';
el.style.margin = '10px';

// ✅ 使用 class 批量修改
el.classList.add('active');

// ✅ 使用 transform/opacity 动画(仅触发合成)
el.style.transform = 'translateX(100px)';
el.style.opacity = '0.5';

// ✅ 批量读取时先读再写(避免强制重排)
const h = el.clientHeight; // 读
el.style.height = h + 10 + 'px'; // 写

requestAnimationFrame:

  • 每一帧浏览器渲染前执行,与显示器刷新率同步(通常 60Hz)
  • 用于修改样式、更新动画,避免掉帧
  • 不可见页面自动暂停,节省资源

虚拟列表(Virtual Scroll)

可视区 ┌────────────┐
       │ 上缓冲区    │ ← 提前渲染,保证快速滚动进入的可视性
       ├────────────┤
       │ 可视区渲染  │ ← 用户看到的部分
       ├────────────┤
       │ 下缓冲区    │ ← 提前渲染
       └────────────┘
       ↑         ↑
   scrollTop    scrollTop+clientHeight
  • 仅渲染可视区 + 上下缓冲区(通常各 5~10 个项目)
  • 监听 scroll 事件计算当前可视范围
  • DOM 节点池回收复用,不随数据增长
  • 知名库: react-window / vue-virtual-scroller / @tanstack/react-virtual
// 核心逻辑示意
function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);
  const startIdx = Math.floor(scrollTop / itemHeight);
  const endIdx = Math.min(
    startIdx + Math.ceil(containerHeight / itemHeight) + bufferSize,
    items.length
  );
  const visibleItems = items.slice(startIdx, endIdx);

  return (
    <div style={{ height: containerHeight, overflow: 'auto' }} onScroll={...}>
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        {visibleItems.map((item, i) => (
          <div key={item.id} style={{
            position: 'absolute',
            top: (startIdx + i) * itemHeight,
            height: itemHeight
          }}>
            {item.content}
          </div>
        ))}
      </div>
    </div>
  );
}

防抖(Debounce)与节流(Throttle)

控制方式 行为 推荐场景 推荐值
防抖 停止触发后 N ms 执行一次 输入搜索、窗口 resize 输入 300ms
节流 固定时间间隔内最多执行一次 滚动监听、鼠标移动 滚动 100ms
// Lodash 防抖:输入建议
input.addEventListener('input', debounce(handleInput, 300));

// Lodash 节流:滚动监听
window.addEventListener('scroll', throttle(handleScroll, 100));

Web Worker

  • 运行在独立线程,不阻塞主 UI 线程
  • 通过 postMessage 通信
  • 适用场景: 图像处理、大数据排序、编解码、复杂计算
  • 限制: 无 DOM 访问、无 window/document,不能跨域加载脚本
// main.js
const worker = new Worker('compute.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
  console.log('结果:', e.data);
};

// compute.js
self.onmessage = (e) => {
  const result = complexCalculation(e.data);
  self.postMessage(result);
};

OffscreenCanvas(离屏渲染)

  • 在 Web Worker 中使用 Canvas 渲染
  • 使用 OffscreenCanvas 替代主线程 Canvas
  • 适用于复杂图形渲染(图表、游戏)
// 主线程
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('renderer.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);

// renderer.js
self.onmessage = (e) => {
  const canvas = e.data.canvas;
  const ctx = canvas.getContext('2d');
  // 在 Worker 中渲染
};

框架优化

React 优化

React.memo / useMemo / useCallback

手段 作用 何时使用
React.memo 组件级缓存:props 未变不重渲染 纯展示组件、props 频繁引用相等但值不变
useMemo 缓存计算结果 复杂计算、避免对象/数组引用变化导致子组件重渲染
useCallback 缓存函数引用 传给子组件的回调 props
// ✅ 使用 React.memo 避免不必要的重新渲染
const ExpensiveList = React.memo(({ items }) => {
  return items.map(item => <div key={item.id}>{item.name}</div>);
});

// ✅ useMemo 缓存复杂计算
const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.price - b.price);
}, [items]);

// ✅ useCallback 保持函数引用稳定
const handleClick = useCallback((id) => {
  setSelected(prev => prev === id ? null : id);
}, []);

// ❌ 避免内联创建对象和函数
<ExpensiveList items={items} onClick={(id) => handleClick(id)} />
//         ↑ 每次渲染创建新函数,破坏 memo
// ✅ 改为 useCallback + 在子组件内部解构

Vue 优化

手段 说明 场景
计算属性 computed 惰性求值,依赖不变不重算 派生状态、复杂表达式
shallowRef 仅追踪 .value 的引用变化,不深层响应 大数据列表、不可变数据
v-memo 缓存模板片段(Vue 3.2+) 固定数据的列表渲染
<script setup>
// ✅ computed 优于模板表达式
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// ✅ shallowRef 处理大数据集
const list = shallowRef(bigArray); // 整体替换时触发更新,不深层响应

// ❌ watch 用于有副作用的场景,不要用于派生数据
// ❌ 模板中的复杂表达式应提取到 computed
</script>

computed vs watch: computed 是惰性求值的派生声明,watch 是侦测变化后执行副作用。能用 computed 就不用 watch。

Suspense + Lazy

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <Loading />
    </template>
  </Suspense>
</template>

React 版:

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<Spinner />}>
      <OtherComponent />
    </Suspense>
  );
}

构建优化

手段 配置 效果
代码分割 splitChunks 分离 vendor / 公共模块
externals externals: { react: 'React' } 通过 CDN 加载,减少 bundle
esbuild/SWC 编译 esbuild-loader / swc-loader 替代 babel-loader,编译速度 10~20x
产物分析 webpack-bundle-analyzer 发现冗余依赖
tree shaking sideEffects: false 删除 dead code

性能监控

Performance API

// Navigation Timing
const nav = performance.getEntriesByType('navigation')[0];
const ttfb = nav.responseStart - nav.requestStart;
const fcp = nav.domContentLoadedEventEnd - nav.fetchStart;

// PerformanceObserver 监听各种性能事件
const observer = new PerformanceObserver((list) => {
  // long task 检测
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'longtask') {
      console.warn(`Long Task detected: ${entry.duration}ms`, entry);
    }
  }
});
observer.observe({ type: 'longtask', buffered: true });

监控工具

工具 用途 类型
Lighthouse CI CI 中集成性能测试,设置阈值 Synthetic
WebPageTest 多地域/多设备/基于真实浏览器的性能分析 Synthetic
web-vitals 收集真实用户 Core Web Vitals RUM
Sentry Performance 错误 + 性能追踪 RUM

Chrome DevTools Performance 面板

火焰图(Flame Chart)解读

时间 ────────────────────────────────────────────►
┌──────────────────────────────────────────────────┐
│ Main ── 线程活动                                  │
│ ┌──────┐ ┌───────┐ ┌────┐ ┌─────────┐          │
│ │Task A│ │ Task B│ │Idle│ │ Task C  │          │
│ │  ├───┐│ │       │ │    │ │  ├───┐  │          │
│ │  │sub││ │       │ │    │ │  │sub│  │          │
│ └──┴───┘│ └───────┘ └────┘ │  └───┘  │          │
└─────────┴─────────────────┴─────────┴──────────┘
 宽度 = 耗时    高度 = 调用栈深度
  • 宽度表示函数执行时间,越宽越耗时
  • 高度表示调用栈深度,右上角的红色三角标记为长任务(> 50ms)
  • Summary 面板: 各类型活动占比(Scripting/Rendering/Painting/System/Idle)

长任务定位

  1. 在 Performance 面板录制
  2. 查看 Main 线程中的红色角标任务(> 50ms)
  3. 点击查看调用栈,定位具体函数
  4. Bottom-Up 标签页按时间倒序排列最耗时的函数

网络与模拟

  • Network Throttling: 模拟 Slow 3G / Fast 3G / Offline
  • CPU Throttling: 模拟低端设备(4x / 6x slowdown)
  • Disable Cache: 模拟首次加载

查看重绘与 Layer

功能 入口 用途
Paint Flashing Rendering → Paint flashing 高亮显示重绘区域
Layer Borders Rendering → Layer borders 查看合成层(可优化复合)
FPS Meter Rendering → FPS Meter 实时帧率
Scrolling Performance Issues Rendering 滚动卡顿排查