性能优化
前端性能优化策略、代码优化、资源压缩、缓存策略等
性能优化的核心目标:让页面加载更快、交互更流畅、资源消耗更少。本笔记按优化阶段系统梳理,每个方向都配有可落地的代码示例。
目录
| 章节 | 核心内容 |
|---|---|
| 一、性能指标 | 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 面板
使用步骤:
- F12 → Performance 面板
- 点击 Record(录制)
- 执行操作
- 停止录制
- 分析火焰图找性能瓶颈
关键区域:
- 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:首屏优化有哪些手段?
- 关键 CSS 内联(Critical CSS)
- 资源预加载(
preload)- 服务端渲染(SSR)或静态生成(SSG)
- 减少首屏 JS 体积(代码分割 + Tree Shaking)
- HTTP/2 多路复用 + 服务端推送
- CDN + 强缓存
Q5:Web Worker 的使用场景?
CPU 密集型计算,如:大文件解析(CSV/JSON)、图像处理(滤镜/压缩)、加密计算、复杂数据排序。不适合:DOM 操作(Worker 无法访问 DOM)。
Q6:如何定位页面性能瓶颈?
- Lighthouse 看整体评分和建议
- Performance 面板录制找 Long Task 和 Layout 次数
- Network 面板看资源大小和加载时序
performance.getEntriesByType('resource')精确测量各资源加载时间
Q7:preload 和 prefetch 的区别?
preload:高优先级,立即加载当前页必须资源(字体、首屏 CSS)prefetch:低优先级,空闲时预加载下个页面可能需要的资源
Q8:什么是 Long Task?如何优化?
Long Task 指执行时间超过 50ms 的 JS 任务,会阻塞主线程导致页面卡顿。优化方式:
- 任务切片:用
setTimeout/scheduler.yield()分批执行- 复杂计算转到 Web Worker
- 懒加载非关键 JS