← Frontend

性能优化

前端性能优化策略、代码优化、资源压缩、缓存策略等

性能优化的核心目标:让页面加载更快、交互更流畅、资源消耗更少。本笔记按优化阶段系统梳理,每个方向都配有可落地的代码示例。


目录

章节 核心内容
一、性能指标 Web Vitals、FCP、LCP、CLS、TTI
二、网络层优化 HTTP 缓存、CDN、资源压缩、HTTP/2
三、资源加载优化 懒加载、预加载、代码分割、Tree Shaking
四、渲染性能优化 减少回流重绘、合成层、DocumentFragment
五、JavaScript 优化 防抖节流、Web Worker、避免内存泄漏
六、图片优化 格式选择、懒加载、响应式图片
七、性能测量工具 Lighthouse、Performance、DevTools
八、性能优化全景 构建优化、框架层优化、字体、Service Worker、监控 SDK、PRPL
九、面试高频考点 经典面试题与答案

一、性能指标(Web Vitals)

Google 定义的核心网页指标,也是 SEO 排名的参考因素:

指标 全称 含义 目标值
FCP First Contentful Paint 首次内容渲染(第一个文字/图片出现) < 1.8s
LCP Largest Contentful Paint 最大内容渲染(主要内容可见) < 2.5s
FID First Input Delay 首次输入延迟(点击到响应的延迟) < 100ms
CLS Cumulative Layout Shift 累计布局偏移(页面抖动程度) < 0.1
INP Interaction to Next Paint 交互到下次渲染(FID 的升级版) < 200ms
TTFB Time To First Byte 首字节时间(服务器响应速度) < 800ms
// 使用 web-vitals 库采集指标
import { getLCP, getFID, getCLS, getFCP, getTTFB } from 'web-vitals';

getLCP(({ value }) => sendToAnalytics('LCP', value));
getFID(({ value }) => sendToAnalytics('FID', value));
getCLS(({ value }) => sendToAnalytics('CLS', value));

二、网络层优化

1. HTTP 缓存策略

请求来了
  ↓
强缓存命中?(Cache-Control: max-age / Expires)
  ✅ 直接用本地缓存,不发请求(200 from cache)
  ❌ 向服务器发请求
       ↓
     协商缓存命中?(ETag / Last-Modified)
       ✅ 返回 304,用本地缓存
       ❌ 返回 200 + 新资源
// 服务端设置强缓存(Node.js / Express)
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 静态资源缓存 1 年
res.setHeader('Cache-Control', 'no-cache');                  // HTML 不强缓存

// 设置协商缓存
res.setHeader('ETag', fileHash);
res.setHeader('Last-Modified', file.mtime.toUTCString());

最佳实践:

  • HTML 文件:Cache-Control: no-cache(每次协商)
  • 带 hash 的 JS/CSS:Cache-Control: max-age=31536000(永久强缓存)
  • 图片:Cache-Control: max-age=86400(缓存 1 天)

2. CDN(内容分发网络)

CDN 在全球部署边缘节点,用户请求就近返回,减少网络延迟。

<!-- 从 CDN 加载公共库(利用用户可能已缓存过) -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

<!-- DNS 预解析,提前完成域名解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>

3. 资源压缩

// Nginx 配置 Gzip(服务端)
gzip on;
gzip_types text/plain application/javascript application/json text/css;
gzip_min_length 1024;  // 小于 1KB 不压缩,压缩收益小于开销

// Vite 配置 Brotli 压缩(构建时)
import viteCompression from 'vite-plugin-compression';

export default {
  plugins: [
    viteCompression({ algorithm: 'brotliCompress' }) // Brotli 压缩率更高
  ]
}

Gzip vs Brotli: Brotli 压缩率比 Gzip 高 15-25%,现代浏览器均支持,推荐优先使用。

4. 减少请求数

// ❌ 多次小请求
fetch('/api/user');
fetch('/api/orders');
fetch('/api/products');

// ✅ 合并为批量接口
fetch('/api/batch', {
  method: 'POST',
  body: JSON.stringify({ requests: ['user', 'orders', 'products'] })
});

// ✅ 或用 Promise.all 并发(不是合并,但效果接近)
const [user, orders, products] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/orders').then(r => r.json()),
  fetch('/api/products').then(r => r.json()),
]);

三、资源加载优化

1. 代码分割(Code Splitting)

// ❌ 一次性加载所有代码(大 bundle,首屏慢)
import heavyLib from './heavy-lib';

// ✅ 动态导入,按需加载
button.addEventListener('click', async () => {
  const { default: heavyLib } = await import('./heavy-lib');
  heavyLib.run();
});

// ✅ React 路由懒加载
import { lazy, Suspense } from 'react';
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));

// 使用时
<Suspense fallback={<Loading />}>
  <HomePage />
</Suspense>

Vite 配置手动分包:

// vite.config.ts
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          'ui-vendor': ['antd', '@ant-design/icons'],
        }
      }
    }
  }
}

2. 图片/组件懒加载

// 原生图片懒加载
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="..." />

// 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);  // 加载完就停止观察
    }
  });
}, { rootMargin: '100px' });  // 提前 100px 开始加载

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

3. 预加载(Preload / Prefetch)

<!-- preload:当前页面必须用到的关键资源,立即加载 -->
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>
<link rel="preload" href="/critical.css" as="style">

<!-- prefetch:下一个页面可能用到的资源,空闲时加载 -->
<link rel="prefetch" href="/next-page.js">

<!-- preconnect:提前建立连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://api.example.com">
属性 优先级 时机 用途
preload 高 立即 当前页关键资源(字体、首屏 CSS)
prefetch 低 空闲 下一个路由可能需要的资源
preconnect 高 立即 第三方域名预连接
dns-prefetch 低 异步 仅做 DNS 解析,比 preconnect 轻

4. Tree Shaking

只有 ESM 语法才支持 Tree Shaking(静态分析,去除未使用代码):

// ❌ 这样导入会把整个 lodash 打包进来(即使只用了一个函数)
import _ from 'lodash';
_.debounce(fn, 300);

// ✅ 按需导入,Tree Shaking 生效
import debounce from 'lodash-es/debounce';
debounce(fn, 300);

// ✅ 或在 package.json 中标记无副作用
{
  "sideEffects": false
}

四、渲染性能优化

1. 减少回流(Reflow)与重绘(Repaint)

触发条件对比:

操作 触发
修改宽高、位置、字体大小 回流 + 重绘
修改颜色、背景、visibility 仅重绘
transform、opacity 合成层,不触发回流重绘 ✅
// ❌ 每次修改样式都触发回流(3 次)
const el = document.getElementById('box');
el.style.width = '100px';
el.style.height = '100px';
el.style.margin = '10px';

// ✅ 方式一:合并到 class(推荐)
el.className = 'box-active';

// ✅ 方式二:cssText 批量赋值(1 次回流)
el.style.cssText = 'width:100px; height:100px; margin:10px';

// ✅ 方式三:先隐藏,改完再显示
el.style.display = 'none';
// ...多次修改...
el.style.display = 'block'; // 仅 2 次回流

避免强制同步布局(Layout Thrashing):

// ❌ 读写交替,每次读都强制触发回流(浏览器无法批量优化)
for (const el of elements) {
  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';           // 批量写
});

2. DocumentFragment 批量 DOM 操作

// ❌ 每次 appendChild 都触发回流(100 次)
const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  list.appendChild(li); // 每次都回流
}

// ✅ 使用 DocumentFragment,只触发 1 次回流
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  fragment.appendChild(li); // 在内存中操作,不触发回流
}
list.appendChild(fragment); // 一次性插入,只触发 1 次回流

3. 开启 GPU 合成层

/* 让动画走合成层(GPU 处理),不触发回流和重绘 */
.smooth-animation {
  transform: translateZ(0);     /* 开启合成层 */
  /* 或 */
  will-change: transform;       /* 提前告知浏览器 */
  transition: transform 0.3s;
}

/* ✅ 用 transform 代替 left/top */
.box {
  transform: translate(100px, 100px); /* 合成层,无回流 ✅ */
  /* 而不是 */
  /* left: 100px; top: 100px; */ /* 触发回流 ❌ */
}

⚠️ 注意: will-change 不是越多越好,过多合成层占用大量 GPU 内存,反而拖慢性能。只在真正需要动画的元素上使用。

4. 虚拟列表(长列表优化)

渲染大量数据时,只渲染可视区内的 DOM 节点:

// 核心原理
class VirtualList {
  constructor({ data, itemHeight, visibleCount }) {
    this.data = data;
    this.itemHeight = itemHeight;
    this.visibleCount = visibleCount;
    this.startIndex = 0;
  }

  // 根据滚动位置计算当前可见的数据切片
  onScroll(scrollTop) {
    this.startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = this.startIndex + this.visibleCount + 1; // 多渲染 1 个缓冲

    // 只渲染 startIndex ~ endIndex 的数据
    this.renderItems(this.data.slice(this.startIndex, endIndex));
  }
}

// 实际项目推荐使用成熟库
// React: react-window / react-virtualized
// Vue: vue-virtual-scroller

五、JavaScript 优化

1. 防抖与节流

参见「浏览器相关 & 函数式编程.md」中的防抖节流章节。

2. Web Worker(CPU 密集型任务)

JS 是单线程的,耗时计算会阻塞 UI。Web Worker 允许在后台线程执行:

// worker.js(后台线程)
self.onmessage = function (e) {
  const result = heavyCompute(e.data); // 耗时计算,不阻塞 UI
  self.postMessage(result);
};

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

worker.postMessage(bigData); // 发送数据给 Worker
worker.onmessage = function (e) {
  console.log('计算结果:', e.data); // 接收结果
};

// 适用场景:大数据排序、图像处理、加密计算、JSON 解析大文件

3. 避免内存泄漏

// ❌ 场景 1:闭包持有大对象
function leak() {
  const bigData = new Array(1000000).fill('data');
  return () => console.log(bigData.length); // bigData 永不释放
}

// ✅ 用完后手动解除引用
let fn = leak();
fn();
fn = null; // GC 可以回收 bigData

// ❌ 场景 2:忘记移除事件监听
element.addEventListener('click', handler);
// 组件销毁时没有 removeEventListener,handler 持有 DOM 引用,内存泄漏

// ✅ 组件卸载时移除
const handler = () => { ... };
element.addEventListener('click', handler);
// 组件销毁时
element.removeEventListener('click', handler);

// ❌ 场景 3:全局变量意外挂载
function foo() {
  bar = 'global'; // 忘写 var/let/const,意外成为全局变量
}

// ✅ 使用严格模式防止意外全局变量
'use strict';
function foo() {
  bar = 'global'; // ❌ ReferenceError,及时暴露错误
}

4. 合理使用 requestAnimationFrame

// ❌ 用 setTimeout 做动画(帧率不稳定,可能跳帧)
function animate() {
  moveElement();
  setTimeout(animate, 16); // 约 60fps,但实际不准
}

// ✅ 用 requestAnimationFrame(与浏览器渲染同步,帧率精准)
function animate() {
  moveElement();
  requestAnimationFrame(animate); // 下一帧再执行,自动跳过不可见标签页
}

requestAnimationFrame(animate);

六、图片优化

1. 选择正确的格式

场景 推荐格式 原因
照片、复杂图像 WebP / AVIF 比 JPG 小 25-35%,现代浏览器均支持
图标、Logo、简单图形 SVG 无损缩放,体积小
动画 WebP / AVIF 比 GIF 小 80%+
需要透明背景 WebP / PNG PNG 无损,WebP 体积更小
兼容老浏览器 JPG / PNG 作为降级
<!-- 使用 picture 元素提供多格式降级 -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="图片描述" loading="lazy" width="800" height="600">
</picture>

2. 响应式图片

<!-- 根据屏幕宽度加载不同尺寸图片(避免手机加载桌面大图) -->
<img
  src="image-800.jpg"
  srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
  alt="响应式图片"
  loading="lazy"
>

3. 图片尺寸预设(避免 CLS)

<!-- ✅ 设置 width 和 height,浏览器可提前占位,避免布局偏移 -->
<img src="photo.jpg" width="800" height="600" alt="...">

<!-- CSS 中响应式 -->
img {
  width: 100%;
  height: auto; /* 保持宽高比 */
}

七、性能测量工具

Lighthouse

# 命令行使用
npx lighthouse https://example.com --output html --output-path ./report.html

Lighthouse 给出 5 个维度评分:

  • Performance(性能)
  • Accessibility(可访问性)
  • Best Practices(最佳实践)
  • SEO
  • PWA

Chrome DevTools Performance 面板

使用步骤:

  1. F12 → Performance 面板
  2. 点击 Record(录制)
  3. 执行操作
  4. 停止录制
  5. 分析火焰图找性能瓶颈

关键区域:

  • Long Tasks(红色标记):执行超过 50ms 的任务,会阻塞主线程
  • Layout / Paint:回流重绘次数
  • FPS:帧率,低于 30fps 会有明显卡顿

Performance API

// 测量代码执行时间
performance.mark('start');
doSomethingExpensive();
performance.mark('end');
performance.measure('my-task', 'start', 'end');

const [measure] = performance.getEntriesByName('my-task');
console.log(`耗时:${measure.duration.toFixed(2)}ms`);

// 获取资源加载时间
const resources = performance.getEntriesByType('resource');
resources.forEach(r => {
  console.log(`${r.name}: ${r.duration.toFixed(0)}ms`);
});

// 获取导航时间(首屏相关指标)
const nav = performance.getEntriesByType('navigation')[0];
console.log('DNS 解析时间:', nav.domainLookupEnd - nav.domainLookupStart);
console.log('TCP 握手时间:', nav.connectEnd - nav.connectStart);
console.log('TTFB:', nav.responseStart - nav.requestStart);
console.log('DOM 解析时间:', nav.domContentLoadedEventEnd - nav.domLoading);

八、性能优化全景(面试综述)

本节专注于前文未深入展开的优化方向(构建分析、框架层、字体、Critical CSS、Service Worker、监控 SDK 等),与前各章节互为补充,构成完整的性能优化知识体系。

8.1 指标采集(PerformanceObserver 实战)

指标定义与目标值见章节一。本节专注代码采集实现。

// ① LCP 采集(关注最后一条:最终确认的最大元素)
const lcpObserver = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', (lastEntry.renderTime || lastEntry.loadTime).toFixed(0), 'ms');
  console.log('LCP element:', lastEntry.element);
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });

// ② CLS 采集(累加非交互触发的布局偏移)
let clsValue = 0;
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {  // 排除用户主动触发的偏移(如点击展开)
      clsValue += entry.value;
    }
  }
  console.log('CLS:', clsValue.toFixed(4));
}).observe({ type: 'layout-shift', buffered: true });

// ③ INP 采集(替代 FID,统计所有交互中最差的延迟)
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.interactionId) {
      console.log(`INP [${entry.name}]: ${entry.duration.toFixed(0)}ms`);
    }
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });

// ④ Long Task 监控(>50ms 的阻塞任务)
new PerformanceObserver((list) => {
  for (const task of list.getEntries()) {
    console.warn(`Long Task: ${task.duration.toFixed(0)}ms`, task.attribution);
  }
}).observe({ type: 'longtask', buffered: true });

Long Task 常见来源:

  • 长时间运行的事件处理函数(复杂业务逻辑未拆分)
  • 大量 DOM 操作触发连续回流
  • 大 JSON 解析、排序等同步计算
  • 第三方脚本(广告、埋点)阻塞主线程

优化 Long Task:任务切片

// 用 setTimeout(0) 让出主线程,让浏览器在每个 chunk 之间处理渲染
async function processInChunks(data, chunkSize = 500) {
  for (let i = 0; i < data.length; i += chunkSize) {
    processChunk(data.slice(i, i + chunkSize));
    await new Promise(resolve => setTimeout(resolve, 0)); // 让浏览器喘口气
  }
}

// 复杂计算移入 Web Worker,彻底不阻塞主线程(见章节五)

8.2 构建优化(Webpack / Vite)

减少打包体积

策略 说明
SplitChunks 提取公共模块和第三方库,充分利用浏览器缓存
Tree Shaking 移除未使用代码,仅对 ESM(import/export)生效,生产模式自动开启
动态垫片 根据 UA 按需注入 Polyfill(polyfill.io),而非全量引入
作用域提升(Scope Hoisting) 将模块合并到同一函数作用域,减少运行时函数声明开销
资源压缩 HTML/CSS/JS/图片全量压缩(见章节二、六)
// webpack.config.js — 综合优化配置
module.exports = {
  optimization: {
    concatenateModules: true,   // 作用域提升
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
          name: 'react-vendor',
          priority: 20,
        },
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
      },
    },
  },
  module: {
    rules: [{
      test: /\.js$/,
      include: /src/,         // 缩减范围:只处理 src
      exclude: /node_modules/,
      use: [
        'thread-loader',      // 多线程并行编译
        { loader: 'babel-loader', options: { cacheDirectory: true } },
      ],
    }],
  },
};

动态垫片示例:

// polyfill.io 根据 UA 只返回当前浏览器缺少的 Polyfill
// <script src="https://polyfill.io/v3/polyfill.min.js?features=Promise,Array.from"></script>

import HtmlTagsPlugin from 'html-webpack-tags-plugin';
export default {
  plugins: [new HtmlTagsPlugin({
    append: false,
    tags: ['https://polyfill.alicdn.com/polyfill.min.js'],
  })],
};

Bundle 分析工具

// Webpack — webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
  plugins: [new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: true })],
};

// Vite — rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer';
export default {
  plugins: [visualizer({ open: true, gzipSize: true, filename: 'stats.html' })],
};

分析报告重点关注:

  • 某 chunk 异常偏大 → 检查是否整包引入(如 import _ from 'lodash' → 改为按需)
  • node_modules 占比过高 → 考虑 CDN 外链 + externals 排除
  • 同一份代码出现在多个 chunk → 调整 splitChunks.cacheGroups 配置

8.3 框架层优化(React / Vue)

React 优化

① 避免不必要的重渲染

// React.memo:props 无变化则跳过重渲染(浅比较)
const ExpensiveList = React.memo(({ items }) => (
  <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
));

// useMemo:缓存昂贵计算结果
const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.price - b.price),
  [items]  // items 引用不变则复用上次结果
);

// useCallback:缓存函数引用(传给子组件时,避免子组件因函数引用变化重渲染)
const handleDelete = useCallback((id) => {
  dispatch({ type: 'DELETE', id });
}, [dispatch]);

② React 18 并发优化

import { useTransition, useDeferredValue, Suspense, lazy } from 'react';

// startTransition:将非紧急更新标记为低优先级(可被高优先级更新中断)
const [isPending, startTransition] = useTransition();

function handleSearch(e) {
  setInputValue(e.target.value);       // 高优先级:立即更新输入框
  startTransition(() => {
    setSearchQuery(e.target.value);    // 低优先级:搜索结果可以延迟渲染
  });
}

// useDeferredValue:延迟非紧急的衍生值,搜索/过滤场景避免卡顿
const deferredQuery = useDeferredValue(query);
const filteredList = useMemo(
  () => list.filter(item => item.name.includes(deferredQuery)),
  [list, deferredQuery]
);

startTransition vs useDeferredValue:

  • startTransition:控制你自己触发的状态更新(如输入框 onChange)
  • useDeferredValue:延迟外部传入的值(如父组件传来的 props)

Vue 优化

<!-- ① keep-alive:缓存组件实例,避免路由切换时重复渲染和重复请求 -->
<keep-alive :include="cachedViews" :max="10">
  <router-view :key="$route.fullPath" />
</keep-alive>

<!-- ② v-memo(Vue 3.2+):依赖数组不变时跳过整个子树的更新 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
  <!-- 只有 item.id 或 item.selected 改变时才重新渲染 -->
  <HeavyItemComponent :item="item" />
</div>
// ③ shallowRef / shallowReactive:大型列表只监听引用变化,不深度追踪内部属性
import { shallowRef, triggerRef } from 'vue';

const bigList = shallowRef([]);

// 修改内部元素后手动触发更新(性能远优于 ref 的深层追踪)
bigList.value.push(newItem);
triggerRef(bigList);

React vs Vue 框架层优化速查:

需求 React Vue
跳过子组件重渲染 React.memo + useMemo v-memo + computed
缓存组件状态 状态提升 / Zustand keep-alive
低优先级更新 startTransition / useDeferredValue nextTick + v-once
大列表性能 react-window / react-virtualized vue-virtual-scroller
浅层响应式 手动实现 shallowRef / shallowReactive

8.4 脚本加载策略(defer / async)

<!-- ① 普通 script:阻塞 HTML 解析,立即下载并执行 -->
<script src="app.js"></script>

<!-- ② async:并行下载,下载完立即执行(不保证顺序,不等 DOM 完成) -->
<script async src="analytics.js"></script>

<!-- ③ defer:并行下载,等 DOM 解析完后按顺序执行(DOMContentLoaded 前) -->
<script defer src="main.js"></script>

<!-- ④ type="module":默认 defer 行为 + 支持 ESM + 自动严格模式 -->
<script type="module" src="app.mjs"></script>
属性 是否阻塞解析 执行时机 执行顺序 适用场景
无 ✅ 阻塞 立即 顺序 内联关键脚本
async ❌ 下载完立即 不保证 独立第三方脚本(GA、广告、客服)
defer ❌ DOM 解析后 保证顺序 依赖 DOM 的业务脚本
type="module" ❌ DOM 解析后 保证顺序 现代 ESM 应用

最佳实践:

  • 把 <script> 移到 </body> 之前(最低成本优化)
  • 业务脚本统一用 defer,独立埋点/统计脚本用 async
  • 现代项目使用 type="module",天然支持 Tree Shaking + 并行加载

8.5 字体优化

字体是 FCP / LCP 的常见杀手,加载不当会导致 FOIT(字体加载期间文字不可见)或 FOUT(字体加载后突然闪动)。

/* ① font-display 控制加载行为 */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;            /* 先显示备用字体,Inter 加载后替换(推荐) */
  unicode-range: U+0020-007E;    /* 字符子集:只加载 ASCII,减小字体文件体积 */
}
<!-- ② 预加载关键字体,消除 FOIT(字体未加载时文字不可见的白块) -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

font-display 取值速查:

值 等待期 交换期 适用场景
block ~3s(不显示文字) 无限 品牌 logo 字体,宁愿等也不用备用字体
swap 极短 无限 通用推荐,FCP 友好,会有轻微 FOUT
fallback ~100ms ~3s 平衡体验,超时后永久用系统字体
optional ~100ms 0 仅利用缓存,网络差时直接跳过自定义字体
/* ③ 系统字体栈:零加载时间,最快 */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* ④ size-adjust 减少字体替换时的布局跳动(FOUT 优化) */
@font-face {
  font-family: 'Inter-Fallback';
  src: local('Arial');
  size-adjust: 107%;       /* 调整备用字体大小,视觉上接近 Inter */
  ascent-override: 90%;
}

8.6 Critical CSS(关键 CSS 内联)

核心思路: 将首屏可见内容的最小 CSS 直接内联到 <head>,消除渲染阻塞;非关键 CSS 异步加载。

<head>
  <!-- ① 内联首屏 CSS(~1-5KB,工具自动提取) -->
  <style>
    body { margin: 0; font-family: sans-serif; background: #fff; }
    .header { height: 60px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
    .hero { min-height: 400px; display: flex; align-items: center; justify-content: center; }
  </style>

  <!-- ② 非关键 CSS:preload + onload 异步切换(不阻塞渲染) -->
  <link rel="preload" href="/styles/main.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles/main.css"></noscript>
</head>

工具链:

# critical(npm 包)—— 自动提取首屏 CSS 并内联到 HTML
npx critical dist/index.html --base dist/ --inline --width 1300 --height 900 --output dist/index.html

8.7 Service Worker 与离线缓存

Service Worker 运行在独立线程,拦截网络请求,是 PWA 的核心,也是性能优化的高级手段。

// sw.js

// 安装阶段:预缓存关键静态资源
const CACHE_NAME = 'static-v1';
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE_NAME).then(cache =>
      cache.addAll(['/index.html', '/main.js', '/styles.css', '/logo.svg'])
    )
  );
  self.skipWaiting(); // 立即激活,无需等待旧 SW 退出
});

// ① Cache First(缓存优先):适合静态资源
self.addEventListener('fetch', (e) => {
  if (e.request.destination === 'script' || e.request.destination === 'style') {
    e.respondWith(
      caches.match(e.request).then(cached =>
        cached || fetch(e.request).then(res => {
          caches.open(CACHE_NAME).then(c => c.put(e.request, res.clone()));
          return res;
        })
      )
    );
  }
});

// ② Network First(网络优先):适合 API 数据
// fetch(e.request).catch(() => caches.match(e.request))

// ③ Stale While Revalidate(立即返回缓存 + 后台更新)
// 最适合列表数据:用户立即看到内容,同时后台静默刷新缓存

三种缓存策略对比:

策略 速度 数据新鲜度 适用场景
Cache First ⚡ 最快 可能过期 JS / CSS / 字体 / 图片
Network First 较慢 最新 API 接口、用户数据
Stale While Revalidate ⚡ 快 略有延迟 新闻列表、商品数据、配置信息

推荐使用 Workbox — Google 出品的 SW 工具库,一行代码切换策略,还支持后台同步、推送通知等高级功能。


8.8 性能监控 SDK(实战设计)

生产级前端监控 SDK 包含四个维度:

监控类型 采集内容 采集方式
性能监控 FCP / LCP / CLS / INP、资源加载耗时(img/js/css)、接口耗时 PerformanceObserver + Navigation Timing API
错误监控 JS 运行时报错、资源加载失败、未捕获 Promise 异常、框架错误 window.onerror + unhandledrejection + ErrorBoundary
行为监控 PV/UV、点击路径、路由跳转、自定义埋点 事件委托 + History API 钩子
异常监控 页面白屏、页面崩溃、长时间无响应 MutationObserver + Long Task API

数据上报方式对比:

方式 优点 缺点 适用场景
fetch / XHR 通用,可携带大量数据 页面卸载时可能丢失 实时上报、重要事件
Image (1px gif) 天然跨域,轻量 只能 GET,受 URL 长度限制 简单曝光埋点
navigator.sendBeacon ✅ 页面卸载时也能可靠发送 数据量有限(~64KB) 离开页面时的性能/行为数据
class Monitor {
  constructor(reportUrl) {
    this.reportUrl = reportUrl;
    this.queue = [];
    this.init();
  }

  init() {
    // ① 性能数据(LCP/CLS/INP)
    ['largest-contentful-paint', 'layout-shift', 'event'].forEach(type => {
      try {
        new PerformanceObserver((list) => {
          for (const entry of list.getEntries()) {
            this.push({ type, value: entry.value ?? entry.duration, name: entry.name });
          }
        }).observe({ type, buffered: true });
      } catch (_) { /* 浏览器不支持时忽略 */ }
    });

    // ② 运行时错误
    window.addEventListener('error', (e) => {
      this.push({ type: 'js_error', msg: e.message, stack: e.error?.stack, src: e.filename });
    });

    // ③ 未捕获的 Promise 异常
    window.addEventListener('unhandledrejection', (e) => {
      this.push({ type: 'promise_error', msg: String(e.reason) });
    });

    // ④ 页面隐藏时批量上报(比 unload 更可靠)
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') this.flush();
    });
  }

  push(data) {
    this.queue.push({ ...data, ts: Date.now(), url: location.href });
    if (this.queue.length >= 10) this.flush(); // 积累 10 条批量发送
  }

  flush() {
    if (!this.queue.length) return;
    navigator.sendBeacon(this.reportUrl, JSON.stringify(this.queue));
    this.queue = [];
  }
}

// 初始化
new Monitor('/api/monitor/report');

8.9 PRPL 模式

Google 为现代 Web 应用提出的最优加载模式,四个字母代表四个阶段:

字母 全称 实践方式
P Push / Preload HTTP/2 Server Push 或 <link rel="preload"> 提前推送关键资源
R Render 最快渲染首屏:SSR/SSG + Critical CSS 内联
P Pre-cache Service Worker 后台预缓存非首屏路由资源
L Lazy-load 动态 import() + React.lazy / Vue 异步组件按需加载
用户请求首页
  │
  ├─ [P] Server Push / Preload → 关键 CSS、首屏 JS chunk、字体文件
  │
  ├─ [R] 首屏渲染(目标 < 1s)← Critical CSS 已内联,无渲染阻塞
  │
  ├─ [P] SW install → 后台静默预缓存其他路由的 JS/CSS
  │
  └─ [L] 用户切换路由 → dynamic import() → SW 缓存命中 → ⚡ 近乎即时

现代框架对应实现:

  • Vite + React:自动代码分割(React.lazy)+ vite-plugin-pwa 处理 Service Worker
  • Next.js:App Router 自动 RSC Streaming,next/dynamic 懒加载,天然 PRPL
  • Nuxt 3:<NuxtLink prefetch> 自动 prefetch + Nitro 边缘渲染

8.10 渲染层面速查

方向 核心原则 已覆盖章节
CSS 优化 避免深层级选择器(.a .b .c .d)、减少重排触发属性 章节四
DOM 优化 缓存 DOM 计算属性、避免频繁读写交替 章节四
脚本阻塞 依赖 DOM 的脚本用 defer,独立脚本用 async 本章 8.4
回流重绘 批量修改样式、动画用 transform 走合成层 章节四
框架重渲染 React.memo / useMemo / keep-alive / v-memo 本章 8.3
首屏渲染 Critical CSS 内联 + SSR/SSG + preload 关键资源 本章 8.6
离线缓存 Service Worker Cache-First / Stale-While-Revalidate 本章 8.7

九、MVVM 框架层面的性能优化

MVVM 框架(如 Vue、React)提供了响应式数据绑定和组件化开发能力,但不当使用也会带来性能瓶颈。优化可从基础类和技巧类两个维度展开。


9.1 基础类优化

a. 利用框架本身设计思路进行优化

框架的设计哲学本身就内含了性能最佳实践,顺势而为往往比"对抗框架"效果更好。

Vue 方向

① v-if vs v-show

指令 实现方式 适用场景
v-if DOM 节点真实销毁/创建 条件很少改变,初始不渲染可节省开销
v-show display: none 切换 频繁切换显隐,避免重复销毁/创建

💡 提示: 频繁切换用 v-show,条件渲染用 v-if,避免不必要的 DOM 操作。

② key 的正确使用

<!-- ❌ 错误:用 index 作 key,diff 算法无法精准复用节点 -->
<li v-for="(item, index) in list" :key="index">{{ item.name }}</li>

<!-- ✅ 正确:用唯一 ID,帮助 diff 算法精准复用 -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>

③ 合理使用 computed 缓存

export default {
  data() {
    return { list: [1, 2, 3, 4, 5, 6] }
  },
  computed: {
    // ✅ computed 有缓存,依赖不变时不会重新计算
    evenList() {
      return this.list.filter(n => n % 2 === 0)
    }
  }
}

⚠️ 注意: methods 每次调用都会重新执行,过滤/计算密集型操作应放在 computed 中。

④ Object.freeze() 冻结不需要响应式的大数据

export default {
  data() {
    return {
      // ✅ 冻结后 Vue 不会为其设置 getter/setter,节省大量初始化开销
      bigTableData: Object.freeze(fetchHugeList())
    }
  }
}

⑤ 组件懒加载(异步组件)

// Vue 3 异步组件
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent(() =>
  // 仅在组件需要渲染时才加载对应 chunk
  import('./components/HeavyChart.vue')
)
React 方向

① React.memo 防止子组件无效重渲染

// ✅ 仅在 props 变化时才重新渲染
const UserCard = React.memo(({ name, age }) => {
  return <div>{name} - {age}</div>
})

② useMemo / useCallback 缓存计算结果和函数引用

function Dashboard({ data }) {
  // ✅ 依赖项不变时,不会重新计算过滤结果
  const filtered = useMemo(
    () => data.filter(item => item.active),
    [data]
  )

  // ✅ 依赖项不变时,不会生成新的函数引用,避免子组件重渲染
  const handleClick = useCallback((id) => {
    console.log('clicked:', id)
  }, [])

  return <List items={filtered} onClick={handleClick} />
}

③ 虚拟列表(长列表优化)

import { FixedSizeList as List } from 'react-window'

// ✅ 只渲染可视区域内的 DOM 节点,万级数据也流畅
function VirtualTable({ items }) {
  return (
    <List
      height={600}        // 容器高度
      itemCount={items.length}
      itemSize={50}       // 每行高度
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>{items[index].name}</div>
      )}
    </List>
  )
}

🚨 警告: 渲染 1000+ 条数据不做虚拟列表,会导致严重卡顿甚至页面崩溃。


b. 利用第三方工具进行优化

工具 / 方案 框架 作用
webpack-bundle-analyzer 通用 可视化分析打包体积,找出大依赖
vite-plugin-inspect Vue/Vite 检查 Vite 插件转换过程
react-devtools Profiler React 录制渲染过程,定位慢组件
vue-devtools Performance Vue 追踪组件渲染耗时
why-did-you-render React 自动标记不必要的重渲染
unplugin-vue-components Vue 组件按需自动导入,减少手动 import
babel-plugin-import 通用 UI 库(如 antd)按需引入,缩减体积
# 安装 bundle 分析工具
npm install --save-dev webpack-bundle-analyzer

# React 渲染追踪工具
npm install --save-dev @welldone-software/why-did-you-render
// why-did-you-render 使用示例(React)
import React from 'react'

// 开发环境开启,生产环境自动关闭
if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render')
  // 追踪所有 Pure Component 的不必要更新
  whyDidYouRender(React, { trackAllPureComponents: true })
}

9.2 技巧类优化

技巧类优化更多依赖开发者对框架运行机制的深入理解,属于"锦上添花"级别的进阶手段。

① 事件销毁:避免内存泄漏

// Vue 3 Composition API
import { onMounted, onUnmounted } from 'vue'

export function useResizeObserver(el) {
  const observer = new ResizeObserver(callback)

  onMounted(() => {
    // 组件挂载时注册
    observer.observe(el.value)
  })

  onUnmounted(() => {
    // ✅ 组件销毁时务必清除,否则持续占用内存
    observer.disconnect()
  })
}

② 防抖 / 节流处理高频事件

import { debounce, throttle } from 'lodash-es'

// ✅ 搜索框输入:防抖,停止输入 300ms 后再请求
const handleSearch = debounce((keyword) => {
  fetchSearchResults(keyword)
}, 300)

// ✅ 滚动事件:节流,每 100ms 最多触发一次
const handleScroll = throttle(() => {
  updateScrollPosition()
}, 100)

③ 分片渲染(Time Slicing)

// 将大量 DOM 操作分批执行,避免长任务阻塞主线程
function renderInChunks(items, chunkSize = 50) {
  let index = 0

  function renderChunk() {
    const chunk = items.slice(index, index + chunkSize)
    chunk.forEach(item => appendToDOM(item))
    index += chunkSize

    if (index < items.length) {
      // 利用 requestAnimationFrame 在浏览器空闲时继续渲染
      requestAnimationFrame(renderChunk)
    }
  }

  renderChunk()
}

④ 路由懒加载(减少首屏体积)

// Vue Router 4
const routes = [
  {
    path: '/dashboard',
    // ✅ 访问该路由时才加载对应 chunk,首屏不加载
    component: () => import('@/views/Dashboard.vue')
  },
  {
    path: '/report',
    component: () => import(/* webpackChunkName: "report" */ '@/views/Report.vue')
  }
]

💡 提示: 路由懒加载配合 Nginx 的 preload/prefetch 提示,可在用户访问前提前加载关键路由,兼顾首屏速度与切换流畅度。


十、大厂可视化性能监控方案体系

性能监控体系是大厂保障线上质量的核心基础设施,完整方案通常分为四层:埋点上报 → 数据处理 → 可视化展现 → 告警处理。

┌─────────────┐    ┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│  埋点上报    │ →  │  数据处理   │ →  │  可视化展现  │ →  │  告警处理   │
│ 点到点+采集  │    │ 阈值/分类   │    │ 报表/Grafana │    │ 触发+分派   │
└─────────────┘    └─────────────┘    └──────────────┘    └─────────────┘

10.1 埋点上报 — 点到点 + 信息采集

埋点是整个监控体系的数据入口,分为自动埋点和手动埋点两种策略。

采集的核心指标

指标 含义 理想值
FCP (First Contentful Paint) 首次内容绘制 < 1.8s
LCP (Largest Contentful Paint) 最大内容绘制 < 2.5s
FID (First Input Delay) 首次输入延迟 < 100ms
CLS (Cumulative Layout Shift) 累积布局偏移 < 0.1
TTFB (Time to First Byte) 首字节时间 < 800ms
JS 错误率 脚本异常比率 趋近 0%
接口成功率 API 请求成功比率 > 99.9%

上报方式

// 使用 Performance Observer 自动采集 Web Vitals
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    // 采集 LCP 数据并上报
    if (entry.entryType === 'largest-contentful-paint') {
      reportMetric({
        type: 'LCP',
        value: entry.startTime,
        url: location.href,
        userId: getCurrentUserId(),
        timestamp: Date.now()
      })
    }
  })
})

// 观察 LCP、FID、CLS 等核心指标
observer.observe({ type: 'largest-contentful-paint', buffered: true })
// 使用 Beacon API 上报(页面关闭时不丢数据)
function reportMetric(data) {
  const payload = JSON.stringify(data)
  // ✅ sendBeacon 不阻塞页面卸载,不受 XHR 取消影响
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/metrics', payload)
  } else {
    // 降级方案:同步 XHR
    fetch('/api/metrics', { method: 'POST', body: payload, keepalive: true })
  }
}

💡 提示: 推荐使用 web-vitals 官方库简化采集,它已处理好各浏览器兼容性问题。


10.2 数据处理 — 阈值设置 / 数据分类 / 数据重组

原始上报数据量极大,需要经过三步处理才能用于展示和告警。

a. 阈值设置

优秀(Good)  →  正常范围,绿色展示
待改进        →  接近阈值,黄色预警
较差(Poor)  →  超出阈值,红色告警
指标 优秀 待改进 较差
LCP ≤ 2.5s 2.5s ~ 4s > 4s
FID ≤ 100ms 100ms ~ 300ms > 300ms
CLS ≤ 0.1 0.1 ~ 0.25 > 0.25

b. 数据分类

// 按多个维度对上报数据进行分类存储
function classifyMetric(raw) {
  return {
    // 按页面路由分类
    page: extractRoute(raw.url),
    // 按用户网络环境分类
    network: raw.effectiveType,    // '4g' | '3g' | '2g' | 'slow-2g'
    // 按设备类型分类
    device: raw.isMobile ? 'mobile' : 'desktop',
    // 按地理区域分类(用于 CDN 调优)
    region: raw.region,
    // 核心指标值
    value: raw.value,
    metric: raw.type
  }
}

c. 数据重组(聚合计算)

原始数据 (每条上报) → 聚合计算 → 统计指标
                        ↓
              P50 / P75 / P90 / P99 分位值
              (用分位值而非均值,更能反映真实用户体验)
// 计算 P75 分位值示例
function percentile(arr, p) {
  const sorted = [...arr].sort((a, b) => a - b)
  // 取第 p% 位置的值,代表 p% 的用户体验优于此值
  const index = Math.ceil((p / 100) * sorted.length) - 1
  return sorted[index]
}

const lcpValues = [1200, 1500, 2000, 2800, 3500, 5000]
console.log('LCP P75:', percentile(lcpValues, 75)) // 3500ms

⚠️ 注意: 大厂通常以 P75 作为核心 SLA 指标,即保证 75% 的用户体验达标,而非看均值。


10.3 可视化展现

处理后的数据通过报表系统直观呈现,通常有两种方案并存。

a. 自研报表监控

适合业务强绑定的指标,可完全定制 UI 和交互逻辑。

// 基于 ECharts 自研 LCP 趋势图示例
const option = {
  title: { text: 'LCP 趋势(近 7 天 P75)' },
  tooltip: { trigger: 'axis' },
  xAxis: { type: 'time' },
  yAxis: {
    type: 'value',
    name: '耗时(ms)',
    // 标注阈值线
    markLine: {
      data: [{ yAxis: 2500, name: 'Good 阈值' }]
    }
  },
  series: [{
    name: 'LCP P75',
    type: 'line',
    smooth: true,
    // 超阈值部分高亮显示
    markArea: {
      data: [[{ yAxis: 2500 }, { yAxis: 'max' }]]
    }
  }]
}

自研报表适合监控的场景:

  • 业务转化率与性能指标的关联分析
  • 按 A/B 实验分组对比性能
  • 自定义业务埋点(点击、曝光等行为事件)

b. Grafana

Grafana 是业界主流的开源可视化平台,适合基础设施级的通用监控。

特性 自研报表 Grafana
定制灵活度 ⭐⭐⭐⭐⭐ ⭐⭐⭐
接入成本 高(需自研) 低(开箱即用)
数据源支持 自有数据 Prometheus、ES、InfluxDB 等多种
告警集成 需自研 原生支持
适用场景 业务监控 基础设施、服务端性能
# Grafana Dashboard 配置示例(JSON 片段)
{
  "title": "前端性能总览",
  "panels": [
    {
      "type": "graph",
      "title": "LCP P75 趋势",
      "datasource": "Prometheus",
      "targets": [
        {
          "expr": "histogram_quantile(0.75, sum(rate(lcp_bucket[5m])) by (le, page))",
          "legendFormat": "{{page}}"
        }
      ]
    }
  ]
}

💡 提示: 大厂通常两者并存:Grafana 做基础设施大盘,自研系统做业务深度分析。


10.4 告警处理

监控体系的最后一环,确保问题第一时间触达负责人。

a. 告警触发

告警触发通常基于以下规则:

触发条件 示例规则 级别
绝对阈值 LCP P75 > 4000ms P2 告警
环比突变 错误率较上小时上涨 > 50% P1 告警
同比异常 今日 PV 较昨日同期下降 > 30% P2 告警
持续超标 连续 5 分钟 FCP > 3s P1 告警
// 告警规则引擎示例(伪代码)
function evaluateAlertRules(currentMetrics, historicalMetrics) {
  const alerts = []

  // 规则 1:绝对阈值检查
  if (currentMetrics.lcp_p75 > 4000) {
    alerts.push({
      level: 'P1',
      message: `LCP P75 超标:${currentMetrics.lcp_p75}ms > 4000ms`,
      page: currentMetrics.page,
      timestamp: Date.now()
    })
  }

  // 规则 2:环比检查(与上小时对比)
  const errorRateChange =
    (currentMetrics.errorRate - historicalMetrics.lastHour.errorRate)
    / historicalMetrics.lastHour.errorRate

  if (errorRateChange > 0.5) {
    alerts.push({
      level: 'P1',
      message: `错误率环比上涨 ${(errorRateChange * 100).toFixed(1)}%`,
      timestamp: Date.now()
    })
  }

  return alerts
}

b. 触发分派 — 钉钉 / 企微 / 邮件

告警产生后,通过 Webhook 推送至对应渠道,精准通知相关负责人。

// 统一告警分派器
async function dispatchAlert(alert) {
  const handlers = getHandlersByLevel(alert.level)

  await Promise.all([
    // 钉钉机器人
    handlers.dingTalk && sendDingTalkAlert(alert),
    // 企业微信机器人
    handlers.wecom && sendWeComAlert(alert),
    // 邮件(P0/P1 才发邮件,避免骚扰)
    alert.level === 'P0' && sendEmailAlert(alert)
  ])
}

// 钉钉 Webhook 告警
async function sendDingTalkAlert(alert) {
  await fetch('https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      msgtype: 'markdown',
      markdown: {
        title: `【${alert.level}】前端性能告警`,
        text: `
## 🚨 ${alert.level} 性能告警

- **告警内容:** ${alert.message}
- **页面:** ${alert.page}
- **时间:** ${new Date(alert.timestamp).toLocaleString()}
- **处理人:** @${alert.owner}
        `
      },
      at: { atMobiles: [alert.ownerPhone] }
    })
  })
}

⚠️ 注意: 告警要避免"告警疲劳"——过多低质量告警会导致负责人麻木,建议做好告警收敛(相同告警 5 分钟内合并)和分级(P0 电话、P1 IM、P2 邮件)。


总结

MVVM 性能优化
├── 基础类
│   ├── 利用框架设计(v-if/v-show、computed缓存、memo、虚拟列表)
│   └── 利用第三方工具(bundle分析、devtools profiler)
└── 技巧类
    └── 事件销毁、防抖节流、分片渲染、路由懒加载

可视化监控体系
├── 埋点上报(Web Vitals + Beacon API)
├── 数据处理(阈值设置 → 数据分类 → P75聚合)
├── 可视化展现(自研报表 + Grafana)
└── 告警处理(规则引擎 → 钉钉/企微/邮件分派)

💡 最终建议: 性能优化和监控是闭环的——监控发现问题 → 定位瓶颈 → 针对性优化 → 监控验证效果。没有监控的优化是盲目的,没有优化的监控是摆设。


十一、面试高频考点

Q1:如何减少回流和重绘?

批量修改 DOM(DocumentFragment / cssText / class)、避免读写交替(Layout Thrashing)、动画用 transform 触发合成层。

Q2:图片懒加载的实现原理?

初始时只渲染 data-src 占位,用 IntersectionObserver 监听元素进入视口,进入时将 data-src 赋给 src。原生方式:<img loading="lazy">。

Q3:什么是 CLS?如何解决?

CLS(累计布局偏移)= 页面内容在加载过程中突然移动。常见原因:图片无尺寸、字体加载闪动、动态插入内容。解决:预设图片 width/height、使用 font-display: swap、避免在已有内容上方动态插入元素。

Q4:首屏优化有哪些手段?

  1. 关键 CSS 内联(Critical CSS)
  2. 资源预加载(preload)
  3. 服务端渲染(SSR)或静态生成(SSG)
  4. 减少首屏 JS 体积(代码分割 + Tree Shaking)
  5. HTTP/2 多路复用 + 服务端推送
  6. CDN + 强缓存

Q5:Web Worker 的使用场景?

CPU 密集型计算,如:大文件解析(CSV/JSON)、图像处理(滤镜/压缩)、加密计算、复杂数据排序。不适合:DOM 操作(Worker 无法访问 DOM)。

Q6:如何定位页面性能瓶颈?

  1. Lighthouse 看整体评分和建议
  2. Performance 面板录制找 Long Task 和 Layout 次数
  3. Network 面板看资源大小和加载时序
  4. performance.getEntriesByType('resource') 精确测量各资源加载时间

Q7:preload 和 prefetch 的区别?

  • preload:高优先级,立即加载当前页必须资源(字体、首屏 CSS)
  • prefetch:低优先级,空闲时预加载下个页面可能需要的资源

Q8:什么是 Long Task?如何优化?

Long Task 指执行时间超过 50ms 的 JS 任务,会阻塞主线程导致页面卡顿。优化方式:

  • 任务切片:用 setTimeout/scheduler.yield() 分批执行
  • 复杂计算转到 Web Worker
  • 懒加载非关键 JS