性能优化
从指标定义、瓶颈定位到方案落地,构建性能面试的完整回答路径。
性能优化题最怕一上来就罗列手段。更好的回答顺序:先讲你关注的指标,再定位瓶颈在网络、渲染还是脚本执行,最后说具体方案和验证方式。
高频主线
- Core Web Vitals 三大指标的含义、阈值与优化方向
- 关键渲染路径:HTML/CSS/JS 如何变成屏幕像素
- 资源加载策略:如何缩短首屏时间
- 重排 / 重绘 / 合成:触发条件与代价排序
- 构建优化:如何减少打包体积和运行时开销
- 框架级优化:Vue / React 渲染模型如何影响 CWV
- 水合与 SSR:隐藏成本与应对策略
- 性能 API:如何测量和监控真实用户体验
Core Web Vitals — 用户体验的三个维度
| 指标 | 全称 | 衡量维度 | 良好 | 需改进 | 较差 |
|---|---|---|---|---|---|
| LCP | Largest Contentful Paint | 加载 — 最大内容渲染时间 | ≤ 2.5s | 2.5s~4s | > 4s |
| INP | Interaction to Next Paint | 交互 — 用户操作到下一帧绘制延迟 | ≤ 200ms | 200ms~500ms | > 500ms |
| CLS | Cumulative Layout Shift | 视觉稳定性 — 意外布局偏移总分 | ≤ 0.1 | 0.1~0.25 | > 0.25 |
INP 于 2024 年 5 月正式取代 FID 成为核心指标。FID 只测首次交互延迟,INP 测量所有交互的完整过程(输入延迟 + 事件处理 + 绘制延迟)。FID 是 TTI 对用户影响的"真实度量"——如果用户在 TTI 时刻未交互,TTI 对用户无实际影响。
辅助指标 — 补量中间过程
| 指标 | 含义 | 良好阈值 | 数据来源 | 功能角色 |
|---|---|---|---|---|
| TTFB | 首字节时间(DNS + TCP + TLS + 服务器处理) | ≤ 800ms | 字段 + 实验室 | 衡量服务端响应速度 |
| FCP | 首次内容绘制 | ≤ 1.8s | 字段 + 实验室 | LCP 的前置指标 |
| TBT | 总阻塞时间(FCP~TTI 间长任务超出 50ms 部分之和) | ≤ 200ms | 实验室 | INP 的实验室预测指标 |
| TTI | 可交互时间 | ≤ 5s | 实验室 | 需 5s 安静窗口计算 |
| FMP | 首次有意义绘制 | — | — | Lighthouse 6.0 已弃用,被 LCP 替代 |
TTI 计算规则:FCP 之后,找到至少 5 秒的安静窗口(无长任务 + 不超过 2 个进行中的 GET 请求),再往前回溯到最后一个长任务结束时间。TBT 是 FID/INP 的实验室预测指标——没有真实用户交互数据时,TBT 可预测交互延迟。
关键渲染路径 — 从字节到像素
浏览器将 HTML、CSS、JavaScript 转换为屏幕像素的完整流程:
HTML 字节 → Token → Node → DOM 树
CSS 字节 → Token → Node → CSSOM 树
DOM + CSSOM → 渲染树 → 布局 → 绘制 → 合成阻塞关系 — 谁挡住了渲染
| 资源 | 阻塞行为 | 优化方式 |
|---|---|---|
| CSS | 阻塞渲染(CSSOM 完成前不显示内容,避免 FOUC) | 内联关键 CSS、非关键 CSS 异步加载 |
| 同步 JS | 阻塞 DOM 解析 | defer / async |
| async JS | 不阻塞解析,下载完立即执行(顺序不保证) | 独立第三方脚本 |
| defer JS | 不阻塞解析,DOM 完成后按序执行 | 业务脚本 |
CSS 阻塞 JS 执行:浏览器必须等 CSSOM 完成后才执行 JS,因为 JS 可能读取样式信息。加载顺序:CSS → JS → 渲染。
优化三原则
- 减少关键资源数量:内联关键 CSS、
defer非关键 JS - 减少关键资源体积:压缩、Tree-shaking、代码分割
- 缩短关键路径长度:
preload预加载关键资源、减少依赖链深度
<link rel="preload" href="critical-font.woff2" as="font" crossorigin>
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.example.com">资源加载优化 — 缩短首屏时间
资源提示 — 控制加载优先级
| 提示 | 作用 | 优先级 | 适用场景 |
|---|---|---|---|
dns-prefetch | DNS 预解析 | 低 | 第三方域名 |
preconnect | DNS + TCP + TLS 预建立连接 | 中 | CDN、API 服务器 |
preload | 预加载当前页面必需资源 | 高 | 关键字体、英雄图 |
prefetch | 预获取未来可能需要的资源 | 低 | 下一页路由资源 |
modulepreload | 预加载 ES Module | 高 | 关键模块 |
preload 优先级高,加载当前页面必需但出现较晚的资源;prefetch 优先级低,加载后续路由可能需要的资源。两者不是随便用,而是有明确的优先级和场景区分。
图片优化 — 最大的体积优化空间
| 策略 | 做法 | 收益 |
|---|---|---|
| 现代格式 | WebP / AVIF(比 JPEG 小 50%+) | 减少传输体积 |
| 响应式图片 | <picture> + srcset + sizes | 按设备加载合适尺寸 |
| 懒加载 | loading="lazy" + Intersection Observer | 缩短关键路径 |
| 尺寸声明 | <img> 设置 width + height 或 aspect-ratio | 避免 CLS |
| 缩略图策略 | 大图先用缩略图,hover 时才加载全图 | 按交互意图按需加载 |
| 图片压缩 | image-webpack-loader / TinyPNG | 减少文件体积 |
缩略图策略不同于懒加载(基于可视区域),它是基于用户交互意图——如果用户从未 hover,就节省了下载大图的时间。
代码分割与懒加载 — 按需加载
// 路由级懒加载
const routes = [
{ path: '/dashboard', component: () => import('./Dashboard.vue') }
]
// React 组件懒加载
const HeavyChart = React.lazy(() => import('./HeavyChart'))
// 动态导入 — 体积大但不一定用到的模块
import('./toastify').then((module) => {
module.toast('Hello World')
})<!-- 关键 CSS 内联,非关键 CSS 异步加载 -->
<style>/* 关键 CSS 内联在 head */</style>
<link rel="preload" href="non-critical.css" as="style"
onload="this.rel='stylesheet'">
<!-- 或使用 media 属性延迟 -->
<link rel="stylesheet" href="print.css" media="print">/* font-display: swap 避免 FOIT(不可见文本闪烁) */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}渲染优化 — 减少像素计算开销
重排 / 重绘 / 合成 — 代价递减的三层
| 类型 | 触发条件 | 代价 | 优化方式 |
|---|---|---|---|
| 重排(Layout) | 改变几何属性(宽高、位置、边距) | 高 | 批量读写分离、避免逐条改样式 |
| 重绘(Paint) | 改变外观属性(颜色、阴影、背景) | 中 | 合理分层、减少绘制区域 |
| 合成(Composite) | transform / opacity 动画 | 低 | GPU 处理,不触发重排重绘 |
动画只用 transform 和 opacity——它们触发合成而非重排/重绘,是性能最优的动画属性。避免用 top/left/width/height 做动画。
减少重排的操作策略
| 做法 | 原因 |
|---|---|
| class 切换替代逐条改样式 | 单次重排而非多次 |
复杂操作前 display:none 隐藏 | 隐藏元素操作不触发重排 |
| DocumentFragment 批量插入 | 一次性插入,减少重排次数 |
| 先读后写,读写分离 | 避免强制同步布局(Layout Thrashing) |
contain CSS 属性 | 限制布局和绘制范围,隔离子树 |
合成层与 GPU 加速 — 跳过重排重绘
触发合成层的条件:
- 3D
transform(translate3d、translateZ) will-change属性position: fixed<video>、<canvas>、CSSopacity动画
will-change 只在即将变化时设置,变化完成后移除。过度使用导致内存消耗过大,每个合成层都需要独立内存。
高频事件优化 — 控制调用频率
| 方式 | 特点 | 适用场景 |
|---|---|---|
| 防抖(debounce) | 一段时间后只执行一次,多次变最后一次 | 搜索输入、resize |
| 节流(throttle) | 固定频率执行,多次变规定时间内一次 | scroll、mousemove |
| 事件委托 | 父元素统一监听,减少注册数量 | 列表项点击 |
虚拟滚动 — 大列表的必选项
大量数据列表只渲染视口内元素,减少 DOM 节点数。Vue 可用 vue-virtual-scroller,React 可用 react-window(比 react-virtualized 更轻量,同一作者)。
框架级性能优化 — Vue / React 如何影响 CWV
框架的渲染模型直接决定了 Core Web Vitals 的表现。理解框架如何影响指标,才能在架构层面做出正确选择。
框架渲染模型对 CWV 的影响
| 指标 | Vue 3 | React | 影响机制 |
|---|---|---|---|
| LCP | 编译优化减少 JS 体积 → 更快的首次渲染 | 运行时 Reconcile → 需更多 JS 执行时间 | 框架 JS 体积和执行时间直接影响 LCP |
| INP | 响应式精确更新 → 最小化重渲染范围 | setState 触发子树协调 → 可能过度渲染 | 更新粒度越细,交互延迟越低 |
| CLS | v-memo / shallowRef 减少不必要更新 | useMemo / React.memo 减少重渲染 | 无效更新导致 DOM 抖动 → CLS 升高 |
LCP — 框架层面的优化
// 路由懒加载 — 减少首屏 JS 体积
const routes = [
{ path: '/dashboard', component: () => import('./Dashboard.vue') }
]
// 异步组件 — 按需加载重型组件
const HeavyChart = defineAsyncComponent(
() => import('./HeavyChart.vue')
)
// shallowRef — 大型数据避免深层代理开销
const bigList = shallowRef<Item[]>([])
// v-once — 纯静态内容跳过更新
// <span v-once>{{ initialValue }}</span>// React.lazy + Suspense — 代码分割
const HeavyChart = React.lazy(() => import('./HeavyChart'))
function App() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)
}
// useMemo — 缓存计算结果,减少重渲染开销
const processed = useMemo(
() => heavyCalc(data), [data]
)
// React.memo — 跳过 props 未变的重渲染
const Item = React.memo(({ value }) => <li>{value}</li>)LCP 的核心瓶颈是"最大的可见内容"何时出现。框架层面能做的是:减少首屏 JS 体积(代码分割)、减少 JS 执行时间(编译优化 / 缓存计算)、避免阻塞渲染的同步脚本。
INP — 减少交互延迟
// v-memo — 依赖不变则跳过更新,减少重渲染
// <div v-for="item in list" :key="item.id"
// v-memo="[item.selected]">
// <ExpensiveComponent :item="item" />
// </div>
// shallowRef — 只追踪 .value,不深层代理
const data = shallowRef({ items: [] })
// 嵌套修改不触发更新,需整体替换
data.value = { items: newData }
// effectScope — 批量管理副作用,一次性清理
const scope = effectScope()
scope.run(() => {
watch(source, callback)
// ...更多 watch/computed
})
scope.stop() // 一次性释放所有// useTransition — 标记低优先级更新,不阻塞用户输入
const [isPending, startTransition] = useTransition()
function handleSearch(query) {
startTransition(() => {
setSearchResults(heavyFilter(query))
})
// 输入框立即响应,搜索结果延迟更新
}
// useDeferredValue — 延迟渲染某个值
const deferredQuery = useDeferredValue(query)
// 搜索列表用 deferredQuery,输入框用 query
// flushSync — 强制同步更新(慎用)
flushSync(() => setCount(c => c + 1))
// 之后的代码能访问更新后的 DOMINP 的核心是"交互到绘制"的延迟。Vue 通过编译期标记(PatchFlag + Block Tree)精确更新,React 通过优先级调度(Lane + useTransition)让紧急更新优先。两种思路不同,目标一致:减少用户感知到的卡顿。
CLS — 避免布局抖动
| 策略 | Vue 3 | React |
|---|---|---|
| 图片占位 | <img> 设 width/height 或 aspect-ratio | 同左 |
| 条件渲染占位 | v-if 切换时用骨架屏占位 | 条件渲染用骨架屏占位 |
| 避免动态注入 | 不在运行时动态插入未预留空间的元素 | 同左 |
| CSS containment | contain: layout style paint | 同左(框架无关) |
| 列表 key 稳定 | v-for 用稳定 key 避免重排 | map 用稳定 key 避免重排 |
CLS 的根因是"渲染后布局发生意外变化"。框架层面的影响主要来自:异步数据加载后 DOM 突然出现、条件渲染切换导致元素位移。解法是预留空间(骨架屏 + 固定尺寸)和稳定 key。
水合(Hydration)— SSR 的隐藏成本
SSR 虽然改善了 LCP,但引入了水合开销:客户端需要重新执行 JS 恢复交互能力。水合期间会阻塞 INP。
SSR 流程:服务端渲染 HTML → 传输 → 客户端显示(可读但不可交互)→ 水合(JS 执行绑定事件)→ 可交互
↑ LCP 在此 ↑ TTI 在此 ↑ INP 受水合影响| 策略 | 原理 | 适用场景 |
|---|---|---|
| 选择性水合 | 只水合可见区域的交互组件 | 大页面 + 部分交互 |
| Islands Architecture | 页面由独立交互"岛屿"组成,非岛屿纯静态 | 内容站 + 少量交互 |
| 流式 SSR | HTML 分块传输,先到先渲染 | 长页面 + 慢数据 |
| Resumability | 不重新执行 JS,直接恢复状态(Qwik) | 极致首屏性能 |
// Vue 3.3+ 流式 SSR
import { renderToWebStream } from 'vue/server-renderer'
// Nuxt 3 默认启用流式渲染
// 组件级 Suspense 边界实现分段传输// React 18 Streaming SSR
import { renderToPipeableStream } from 'react-dom/server'
renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
// HTML shell 就绪,开始流式传输
pipe(res)
}
})
// Selective Hydration — Suspense 边界实现
<Suspense fallback={<Loading />}>
<Comments /> {/* 延迟水合 */}
</Suspense>水合是 SSR 的隐藏成本——HTML 快速显示改善了 LCP,但水合期间 JS 执行阻塞主线程,影响 INP。选择 SSR 时不只看 LCP,还要评估水合对交互延迟的影响。Resumability(Qwik)和 Islands Architecture(Astro)是解决水合成本的两种新思路。
构建优化 — 减少产物体积
打包体积优化 — 先分析再优化
先用 webpack-bundle-analyzer 分析包体积,再针对性优化:
| 策略 | 做法 | 收益 |
|---|---|---|
| 压缩资源 | HTML: html-webpack-plugin、CSS: css-minimizer-webpack-plugin、JS: terser-webpack-plugin | 减少文件体积 |
| Tree Shaking | Webpack 自带支持,但库本身需 ESM 格式 | 消除未使用代码 |
| 动态导入 | import() 按需加载"大且非首屏必需"的模块 | 减少首屏包体积 |
| 更小库替代 | dayjs 替代 moment、lodash-es 替代 lodash | 直接减少依赖体积 |
| 版本升级 | 保持关键库最新,但需评估 breaking change | 间接减少体积 |
| 去除生产日志 | 自定义日志类生产环境关闭 / Webpack 插件移除 console.log | 减少体积 + 防止泄露 |
Tree Shaking 不仅取决于 Webpack 配置,还取决于库本身的模块格式。lodash 用 CJS 不支持 Tree Shaking,需用 lodash-es(ESM 格式)替代。生产日志不是简单"删不删"的问题,而是可调试性与性能的平衡——建议自定义日志类,生产默认关闭但可通过浏览器参数打开。
渲染模式选择 — 按场景权衡
| 方式 | 做法 | 适用场景 | 成本 |
|---|---|---|---|
| CSR | 客户端渲染,JS 生成 DOM | 不需要 SEO 的应用 | 低 |
| SSR | 服务端直接返回渲染好的 HTML | SEO + 首屏速度要求 | 近乎重构 |
| 预渲染 | 构建时生成静态 HTML | 改动较小的 SEO 方案 | 中 |
| SSG | 静态站点生成,构建时全量生成 | 内容不变的页面 | 中 |
| ISR | 增量静态再生,按需重新生成 | 内容偶尔更新的页面 | 中 |
SSR 近乎重构网站,中途接入成本极高。如果不需要 SEO,客户端渲染即可;想改善 SEO/首屏但改动预算有限,预渲染是低成本替代。不要盲目选择 SSR,而是根据场景权衡。
网络优化 — 减少传输延迟
缓存策略 — 避免重复请求
| 类型 | 机制 | 响应头 | 适用场景 |
|---|---|---|---|
| 强缓存 | 不发请求,直接用本地 | Cache-Control: max-age=31536000, immutable | 静态资源(带 hash) |
| 协商缓存 | 发请求验证,304 则用本地 | ETag / If-None-Match | HTML、API 响应 |
| SW 缓存 | Service Worker 拦截请求 | Cache API 灵活控制 | 离线方案、PWA |
| bfcache | 前进/后退时直接恢复页面 | 浏览器内置 | 导航回退 |
传输优化 — 减少往返和体积
| 策略 | 做法 | 收益 | 注意事项 |
|---|---|---|---|
| HTTP/2 多路复用 | 单连接并行请求 | 减少连接开销 | 使雪碧图优化过时 |
| HTTP/3(QUIC) | UDP + 0-RTT | 减少握手延迟 | — |
| Gzip / Brotli 压缩 | 文本资源压缩传输 | 减少传输体积 | 前端打包只是第一步,Nginx 也必须开启 |
| CDN 边缘部署 | 静态资源就近分发 | 降低 TTFB | 免费 CDN 需考虑兜底方案 |
| 103 Early Hints | 最终响应前发送资源提示 | 提前预加载 | — |
| 减少不必要请求 | 请求去重与按需请求 | 减少无效请求 | 不是盲目减少,而是减少"不必要的" |
Gzip 压缩需前后端配合:仅前端通过 Webpack 插件生成 .gz 文件不够,Nginx 也必须开启 gzip 才能生效。CDN 兜底:免费 CDN 链接存在可用性风险,必须有兜底方案。雪碧图已过时:HTTP2 多路复用使雪碧图失去意义,且维护成本高,现代项目不应再使用。
运行时优化 — 减少主线程阻塞
长任务拆分 — 让主线程喘口气
任务超过 50ms 会阻塞主线程,影响 INP。拆分策略:
// scheduler.yield()(新 API)
async function processItems(items) {
for (const item of items) {
doWork(item)
await scheduler.yield() // 让主线程处理其他任务
}
}
// requestIdleCallback(低优先级)
requestIdleCallback(() => { /* 非紧急任务 */ })Web Workers — CPU 密集型任务的后台线程
const worker = new Worker('compute.js')
worker.postMessage({ data: largeDataset })
worker.onmessage = (e) => { result.value = e.data }Web Worker 适合与 UI 无关的长时间运行脚本(大数据计算、图像处理),但不能操作 DOM。通信通过 postMessage,数据传递需序列化。
性能 API 与测量 — 量化优化效果
PerformanceObserver — 监听关键指标
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries()
const last = entries[entries.length - 1]
console.log('LCP:', last.startTime)
}).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
}
}).observe({ type: 'layout-shift', buffered: true })
// INP
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('INP duration:', entry.duration)
}
}).observe({ type: 'event', buffered: true })
// 长任务检测
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Long task:', entry.duration)
}
}).observe({ type: 'longtask', buffered: true })Navigation Timing — 页面加载时间线
redirectStart → domainLookupStart → connectStart →
secureConnectionStart → requestStart → responseStart(TTFB) →
responseEnd → domInteractive → domContentLoaded → domComplete → loadEventEnd旧版 window.performance.timing API 将被废弃,应迁移到 PerformanceNavigationTiming API。基于旧 API 的采集库需要升级,新项目应直接使用新 API。
测量工具 — 实验室 + 字段双视角
| 工具 | 类型 | 用途 |
|---|---|---|
| Lighthouse | 实验室 | 综合审计,生成评分和优化建议(建议无痕模式测试) |
| DevTools Performance | 实验室 | 帧率分析、长任务检测、渲染管线 |
| DevTools Network | 实验室 | 瀑布图、缓存分析 |
| WebPageTest | 实验室 | 多地域、多设备测试,高级瀑布图 |
| PageSpeed Insights | 实验室 + 字段 | Lighthouse + CrUX 结合 |
| web-vitals 库 | 字段 | Google 官方 JS 库,简化 CWV 采集 |
| Search Console | 字段 | Core Web Vitals 评估报告 |
// web-vitals 库采集 + sendBeacon 上报
import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals'
function reportMetric(metric) {
navigator.sendBeacon('/api/metrics', JSON.stringify(metric))
}
onLCP(reportMetric)
onINP(reportMetric)
onCLS(reportMetric)
onTTFB(reportMetric)sendBeacon 比 XHR 更适合性能数据上报,因为它不受页面卸载影响,确保数据不丢失。RUM(真实用户监控)反映真实用户体验,合成监控适合回归测试——两者结合才能全面评估。
性能优化体系总结 — 七层金字塔
| 层级 | 关注点 | 关键手段 |
|---|---|---|
| 网络与传输 | TTFB、连接开销 | CDN(带兜底)、缓存、HTTP/2+、Brotli |
| 资源加载 | FCP、LCP | preload/prefetch、懒加载、代码分割、图片优化 |
| 渲染路径 | 阻塞、关键路径 | 内联关键 CSS、defer JS、减少关键资源 |
| 框架层 | CWV 与渲染模型 | Vue 编译优化、React Lane 调度、选择性水合 |
| 运行时 | INP、长任务 | 批量 DOM、虚拟滚动、合成层、防抖节流、Web Worker |
| 构建 | 包体积 | 压缩、Tree Shaking、动态导入、更小库替代、去日志 |
| 测量与监控 | 全指标 | Performance API、Lighthouse、web-vitals + sendBeacon、RUM |
性能是持续过程,不是一次性任务。优先优化对用户感知影响最大的指标,实验室数据(Lab)与字段数据(Field)结合分析,建立性能预算防止回退。
项目表达模板 — 现象 → 定位 → 方案 → 结果
| 步骤 | 内容 | 举例 |
|---|---|---|
| 现象 | 描述用户感知的性能问题 | 首屏加载 4s+、列表滚动卡顿 |
| 定位 | 用指标和工具找到瓶颈 | Lighthouse 发现 LCP 4.2s、DevTools 定位长任务 |
| 方案 | 针对瓶颈的具体优化 | 图片 WebP + preload、代码分割、虚拟滚动 |
| 结果 | 用数据验证优化效果 | LCP 从 4.2s → 1.8s、INP 从 350ms → 120ms |
常见误区 — 14 个高频踩坑点
| 误区 | 正解 |
|---|---|
setTimeout 做动画 | 用 requestAnimationFrame |
will-change 随处设置 | 只在即将变化时设置,完成后移除 |
| 只看实验室数据 | 实验室 + 字段数据结合分析 |
| 图片不设尺寸 | 必须设 width/height 或 aspect-ratio,避免 CLS |
| CSS 放底部 | CSS 必须在 <head> 中,否则阻塞渲染 |
| 懒加载一切 | 关键资源不应懒加载,首屏英雄图应 preload |
| 盲目减少 HTTP 请求 | 应减少"不必要的"请求,而非盲目减少 |
| Gzip 前端打包即可 | Nginx 也必须开启 gzip 才能生效 |
| 需要 SEO 就用 SSR | 预渲染是低成本替代,SSR 近乎重构 |
| 还在用雪碧图 | HTTP2 多路复用使雪碧图过时,维护成本也高 |
用 moment 处理日期 | dayjs 体积仅 2KB,moment 230KB+ |
| CSS 选择器需要深度优化 | 最慢和最快差别极小,避免反模式即可 |
| SSR 一定能改善所有指标 | SSR 改善 LCP 但水合增加 INP,需权衡 |
框架优化就是加 memo | 优化要从渲染模型和更新粒度出发,而非到处加缓存 |