【performance 面试考点】让面试官眼前一亮的 performance 性能优化
在前端面试中,性能优化绝对是大厂高频考点 —— 不仅能考察候选人对浏览器底层原理的理解,还能看出其工程化落地能力。很多同学只知道 “要做性能优化”,却讲不清 “为什么要做”“怎么做才优雅”“底层逻辑是什么”。
本文会从浏览器渲染原理切入,覆盖「重绘重排」「资源加载」「JS 执行」「框架优化」「首屏优化」等核心模块,结合真实代码案例和面试高频考点,帮你构建一套 “有深度、能落地” 的性能优化知识体系,让面试官一听就知道你是 “懂行的”。
一、基础核心:搞懂重绘重排,从渲染底层优化
要做性能优化,首先得明白浏览器是怎么渲染页面的 —— 这是所有视觉相关优化的基石。浏览器渲染流水线大致分为 5 步:
解析HTML → 构建DOM树 → 解析CSS → 构建CSSOM树 → 合成渲染树 → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)
其中,布局(Layout) 和 绘制(Paint) 是性能消耗的重灾区,对应我们常说的「重排」和「重绘」。
1. 重绘 vs 重排:定义与区别(面试必背)
很多同学会混淆这两个概念,记住一句话: “几何变了是重排,样式变了是重绘” ,具体对比如下:
| 维度 | 重绘(Repaint) | 重排(Reflow/Layout) |
|---|---|---|
| 触发原因 | 元素样式改变,但不影响几何属性 | 元素几何属性改变(位置 / 大小 / 显示) |
| 性能消耗 | 低(仅重新绘制像素) | 高(需重新计算布局 + 重绘) |
| 关联关系 | 重绘不一定触发重排 | 重排一定会触发重绘 |
2. 重绘重排优化:6 个实战方案(带代码)
面试时不仅要讲 “是什么”,更要讲 “怎么优化”,以下方案均来自真实项目实践:
方案 1:合并 DOM 样式操作(避免多次触发)
浏览器会有「渲染队列」优化,尝试合并多次样式修改,但手动合并能更彻底避免意外触发。
❌ 错误示例(可能触发多次重排):
const el = document.getElementById('box');
el.style.width = '200px'; // 触发重排
el.style.height = '200px'; // 再次触发重排
el.style.backgroundColor = 'red'; // 触发重绘
✅ 正确方案(1 次触发):
// 方案1:用cssText合并样式
el.style.cssText = 'width: 200px; height: 200px; background-color: red;';
// 方案2:添加class(更易维护,推荐)
el.className = 'box--active'; // 样式写在CSS中
方案 2:用文档碎片(DocumentFragment)批量操作 DOM
直接向页面中添加多个 DOM 元素时,每添加一个都会触发一次重排;而DocumentFragment是 “虚拟 DOM 容器”,不在真实 DOM 树中,批量添加后再插入页面,仅触发 1 次重排。
// 需求:向页面添加100个列表项
const fragment = document.createDocumentFragment(); // 虚拟容器
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `列表项 ${i + 1}`;
fragment.appendChild(li); // 操作虚拟容器,无重排
}
// 仅1次重排:将批量元素插入真实DOM
document.querySelector('ul').appendChild(fragment);
方案 3:脱离文档流后操作 DOM(减少布局计算)
如果要对某个元素做大量样式修改,先将其 “移出” 文档流(避免影响其他元素的布局计算),操作完成后再 “移回”,仅触发 2 次重排(移出 + 移回)。
常用的 “脱离文档流” 方式:
- position: absolute/fixed:脱离文档流,但仍在渲染树中
- display: none:直接从渲染树中移除(完全不可见)
const el = document.getElementById('box');
// 1. 脱离文档流(1次重排)
el.style.position = 'absolute';
// 或 el.style.display = 'none';
// 2. 大量样式操作(无重排)
el.style.width = '300px';
el.style.height = '300px';
el.style.border = '1px solid #000';
// 3. 恢复文档流(2次重排)
el.style.position = 'static';
// 或 el.style.display = 'block';
⚠️ 面试考点:visibility: hidden和display: none的区别?
- visibility: hidden:元素不可见,但仍占据布局空间(仅重绘,不重排)
- display: none:元素不可见,且从布局中移除(重排 + 重绘)
方案 4:缓存布局信息(避免 “读写交错”)
浏览器有个 “怪癖”:读取布局属性(如 offsetTop、clientWidth)时,会强制刷新渲染队列,确保拿到最新的布局数据。如果 “读 - 写 - 读 - 写” 交错执行,会触发多次重排。
❌ 错误示例(100 次重排):
const el = document.getElementById('box');
// 每次循环:先读offsetTop(强制刷新)→ 再写top(触发重排)
for (let i = 0; i < 100; i++) {
el.style.top = el.offsetTop + 1 + 'px';
}
✅ 正确方案(1 次重排):
const el = document.getElementById('box');
// 先读:缓存布局信息(1次读取,无重排)
const currentTop = el.offsetTop;
// 再写:循环中仅修改变量,不触发重排
for (let i = 0; i < 100; i++) {
el.style.top = (currentTop + i + 1) + 'px';
}
// 仅1次重排:所有修改一次性生效
方案 5:用 transform/opacity 代替传统样式(跳过布局和绘制)
transform和opacity是特殊属性 —— 它们修改的是「合成层」,不会触发布局(Layout)和绘制(Paint),直接进入合成(Composite)阶段,由 GPU 加速渲染,性能极佳。
| 样式修改方式 | 触发阶段 | 性能消耗 |
|---|---|---|
| el.style.left = '100px' | Layout → Paint → Composite | 高 |
| el.style.transform = 'translateX(100px)' | 仅 Composite | 低(GPU 加速) |
✅ 优化示例(动画场景):
// 差:修改left,触发多次重排
function moveBad() {
let left = 0;
setInterval(() => {
el.style.left = left++ + 'px';
}, 16);
}
// 好:用transform,仅触发合成
function moveGood() {
let x = 0;
requestAnimationFrame(function animate() {
el.style.transform = `translateX(${x++}px)`;
requestAnimationFrame(animate);
});
}
⚠️ 面试考点:为什么 transform 性能好?
因为 transform 属于「合成层属性」,GPU 会为合成层创建独立的纹理缓存,修改时不需要重新计算其他元素的布局,也不需要重绘像素,直接操作 GPU 缓存,速度更快。
方案 6:避免强制同步布局(Forced Synchronous Layout)
除了 “读写交错”,还有些场景会强制浏览器同步执行布局,比如:
- 循环中同时读取多个布局属性(offsetTop、clientHeight、getComputedStyle 等)
- 立即修改样式后立即读取布局属性
优化原则:批量读取 → 批量修改,尽量避免在修改样式后立即读取布局信息。
二、资源加载优化:减少请求、缩小体积、加快速度
资源加载是首屏慢的主要原因之一 —— 大图片、未压缩的 JS/CSS、过多的 HTTP 请求,都会拖慢页面加载速度。以下是 10 个高频优化方案,覆盖图片、脚本、字体等核心资源。
1. 图片优化:从格式到加载全链路
图片是页面资源体积的 “大头”,优化图片能立竿见影减少加载时间。
(1)使用 WebP 格式(体积减少 50%+)
WebP 是谷歌推出的图片格式,相同质量下体积比 JPG 小 30%~50%,比 PNG 小 20%~30%,且支持透明和动图(替代 GIF)。
✅ 实现方式(兼容降级):
用标签实现 WebP 优先加载,不支持的浏览器自动降级为 JPG/PNG:
<picture>
<!-- 支持WebP的浏览器加载 -->
<source srcset="image.webp" type="image/webp">
<!-- 不支持的浏览器降级加载 -->
<img src="image.jpg" alt="示例图片" width="800" height="600">
</picture>
⚠️ 注意:给图片设置固定宽高,避免加载完成后布局偏移(影响 CLS 指标,后面会讲)。
(2)图片懒加载(延迟加载非首屏图片)
首屏加载时只加载 “看得见” 的图片,滚动到可视区域再加载其他图片,减少首屏请求数和流量。
✅ 实现方案:
- 原生方案:给
加loading="lazy"(简单,兼容性好)
<img src="image.jpg" alt="懒加载图片" loading="lazy"
width="800" height="600"
decoding="async"> <!-- 异步解码,不阻塞主线程 -->
- 自定义方案(兼容低版本浏览器):
监听scroll事件,判断图片是否进入视口,动态设置src:
const lazyImages = document.querySelectorAll('img[data-src]');
function lazyLoad() {
lazyImages.forEach(img => {
// 检查图片是否进入视口
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
// 加载图片
img.src = img.dataset.src;
// 移除监听(避免重复加载)
img.removeAttribute('data-src');
}
});
}
// 监听滚动和窗口 resize
window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
// 初始执行一次(首屏可见的图片)
lazyLoad();
(3)图片压缩与裁剪
- 压缩:用工具(如 TinyPNG、Squoosh)压缩图片,去除冗余元数据,不损失视觉质量。
- 裁剪:根据页面展示尺寸提供合适大小的图片,避免 “用 1000px 的图显示 200px 的位置”(浪费带宽)。
2. 脚本加载优化:控制执行顺序,避免阻塞
### 3. 资源预加载 / 预解析:提前 “备货”
针对 “未来可能用到的资源” 或 “当前页面必须的资源”,提前加载或解析,减少后续等待时间。
#### (1)preload:当前页面必须资源,立即加载
用于加载当前页面关键资源(如首屏 CSS、核心 JS),优先级高,不会被浏览器忽略。
```
(2)prefetch:未来页面可能用到的资源,空闲加载
用于加载下一页可能用到的资源(如首页预加载详情页 JS),优先级低,仅在浏览器空闲时加载,不影响当前页面性能。
<!-- 预加载下一页的JS -->
<link rel="prefetch" href="/detail-page.js">
(3)dns-prefetch:预解析 DNS
提前解析域名对应的 IP,减少 DNS 查询时间(DNS 查询通常需要 20~100ms)。
<!-- 预解析CDN域名 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 预解析接口域名 -->
<link rel="dns-prefetch" href="//api.example.com">
(4)preconnect:提前建立 TCP 连接
不仅预解析 DNS,还提前建立 TCP 连接(三次握手)和 TLS 连接(HTTPS 加密),进一步减少后续请求的延迟。
<link rel="preconnect" href="//cdn.example.com" crossorigin>
4. 其他资源优化
- 图标字体库 / SVG 图标:用 Font Awesome 或自定义图标库,代替多个小图标图片,减少 HTTP 请求(1 个字体文件对应所有图标);SVG 图标支持矢量缩放,不失真。
- CSS 优化:提取首屏关键 CSS(Critical CSS),内联到中,避免外部 CSS 阻塞渲染;非关键 CSS 异步加载。
三、JS 执行优化:避免主线程阻塞,提升交互流畅度
JS 运行在主线程,与 DOM 渲染、用户交互共享线程 —— 如果 JS 执行时间过长(超过 50ms),会阻塞渲染和交互,导致页面卡顿、掉帧。
1. 防抖(Debounce)与节流(Throttle):控制函数执行频率
这两个是面试必手写的优化手段,用于解决 “高频事件触发过多函数执行” 的问题(如搜索输入、滚动加载、窗口 resize)。
(1)防抖:多次触发,只执行最后一次
适用场景:搜索输入联想、窗口 resize 回调、按钮防重复提交。
✅ 手写防抖函数(支持立即执行和取消):
function debounce(fn, delay, immediate = false) {
let timer = null; // 闭包保存定时器
const debounced = function(...args) {
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 立即执行:第一次触发时执行
if (immediate && !timer) {
fn.apply(this, args);
}
// 延迟执行:最后一次触发后延迟执行
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; // 执行后清空定时器
}, delay);
};
// 取消防抖(如组件卸载前取消)
debounced.cancel = function() {
if (timer) clearTimeout(timer);
timer = null;
};
return debounced;
}
// 使用示例:搜索输入联想
const searchInput = document.getElementById('search');
const handleSearch = debounce((value) => {
console.log('发送搜索请求:', value);
}, 500); // 输入停止500ms后执行
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
(2)节流:固定时间内,只执行一次
适用场景:滚动加载数据、滚动监听导航栏变化、高频点击按钮。
✅ 手写节流函数(时间戳版):
function throttle(fn, interval) {
let lastTime = 0; // 上一次执行时间
return function(...args) {
const now = Date.now(); // 当前时间
// 间隔时间超过interval,执行函数
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now; // 更新上一次执行时间
}
};
}
// 使用示例:滚动加载
const handleScroll = throttle(() => {
console.log('滚动到指定位置,加载更多数据');
}, 1000); // 每1000ms最多执行一次
window.addEventListener('scroll', handleScroll);
2. Web Workers:把复杂计算 “搬” 到子线程
JS 是单线程,但可以通过Web Workers创建子线程,处理复杂计算(如数据筛选、加密解密、大文件解析),避免阻塞主线程。
核心特性(面试必讲):
- 子线程不能操作 DOM,不能访问window对象(只能访问self);
- 子线程与主线程通过postMessage通信(数据深拷贝,不是共享);
- 子线程可以加载其他脚本(importScripts)。
✅ 实战示例(处理大数据筛选):
// 主线程(main.js)
const worker = new Worker('worker.js'); // 创建子线程
// 1. 向子线程发送数据
const bigData = Array.from({ length: 1000000 }, (_, i) => i); // 100万条数据
worker.postMessage(bigData);
// 2. 接收子线程的计算结果
worker.onmessage = (e) => {
console.log('筛选结果(大于500000的数据):', e.data);
worker.terminate(); // 任务完成,关闭子线程
};
// 子线程(worker.js)
self.onmessage = (e) => {
const data = e.data;
// 复杂计算:筛选大于500000的数据
const result = data.filter(item => item > 500000);
// 向主线程发送结果
self.postMessage(result);
self.close(); // 关闭子线程
};
3. requestAnimationFrame(rAF):优化动画执行
setTimeout/setInterval做动画有缺陷 —— 时间不准(受主线程阻塞影响),可能导致掉帧;而rAF会跟浏览器重绘频率同步(通常 60fps,即 16.67ms 执行一次),后台标签页会暂停执行,节省性能。
✅ 用 rAF 实现平滑动画:
function animateElement(el) {
let x = 0;
function move() {
x += 1;
el.style.transform = `translateX(${x}px)`;
// 如果没到目标位置,继续执行
if (x < 500) {
requestAnimationFrame(move);
}
}
// 启动动画
requestAnimationFrame(move);
}
// 使用
animateElement(document.getElementById('box'));
4. requestIdleCallback(rIC):利用空闲时间执行低优先级任务
rIC会在浏览器 “空闲时间”(即当前帧渲染完成后,还有剩余时间)执行任务,不会阻塞关键渲染和交互,适合执行非紧急任务(如日志上报、数据统计、非关键 DOM 操作)。
✅ 用 rIC 执行低优先级任务:
// 低优先级任务:上报用户行为日志
function reportLog(log) {
console.log('上报日志:', log);
}
// 利用空闲时间执行
if ('requestIdleCallback' in window) {
requestIdleCallback((deadline) => {
// deadline.timeRemaining():当前空闲时间(ms)
while (deadline.timeRemaining() > 0) {
reportLog({ type: 'click', time: Date.now() });
break; // 一次执行一个任务,避免占用过多时间
}
});
} else {
// 兼容低版本浏览器:用setTimeout降级
setTimeout(() => reportLog({ type: 'click', time: Date.now() }), 0);
}
⚠️ 面试考点:React Fiber 和 rIC 的关系?
React Fiber 的核心是 “将渲染任务拆分成小块,在浏览器空闲时间执行”,其底层灵感就来自requestIdleCallback—— 通过时间切片(Time Slicing)避免长时间阻塞主线程,提升交互流畅度。
四、框架层优化:Vue/React 项目专属技巧
在框架项目中,性能优化不仅要做 “通用优化”,还要结合框架特性,避免不必要的组件重渲染。
1. React:用 memo/useMemo/useCallback 减少重渲染
React 组件默认会在 “父组件重渲染” 或 “自身 state/props 变化” 时重渲染,很多时候这种重渲染是不必要的(如 props 未变化)。
(1)React.memo:组件级缓存
用于缓存函数组件,避免 props 未变化时的重渲染(类似类组件的PureComponent)。
// 子组件:用memo包裹,props不变则不重渲染
const Child = React.memo(({ name, onClick }) => {
console.log('Child 重渲染了');
return <button onClick={onClick}>{name}</button>;
});
// 父组件
const Parent = () => {
const [count, setCount] = React.useState(0);
const name = '按钮';
// 注意:如果onClick是匿名函数,每次渲染会生成新引用,导致Child重渲染
// 解决方案:用useCallback缓存函数引用
const handleClick = React.useCallback(() => {
console.log('点击了');
}, []); // 依赖为空,函数引用永久不变
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>加1</button>
{/* 即使Parent重渲染,Child的props(name和handleClick)不变,不会重渲染 */}
<Child name={name} onClick={handleClick} />
</div>
);
};
(2)useMemo:缓存计算结果
用于缓存复杂计算的结果,避免每次渲染都重新计算(如大数据筛选、复杂公式计算)。
const Parent = () => {
const [list, setList] = React.useState(Array.from({ length: 10000 }, (_, i) => i));
const [filterNum, setFilterNum] = React.useState(5000);
// 复杂计算:筛选大于filterNum的数据
// 用useMemo缓存结果,仅当filterNum变化时重新计算
const filteredList = React.useMemo(() => {
console.log('重新筛选数据');
return list.filter(item => item > filterNum);
}, [list, filterNum]); // 依赖项变化才重新计算
return (
<div>
<input
type="number"
value={filterNum}
onChange={(e) => setFilterNum(Number(e.target.value))}
/>
<p>筛选结果长度:{filteredList.length}</p>
</div>
);
};
(3)useCallback:缓存函数引用
用于缓存函数引用,避免因函数引用变化导致子组件(用 memo 包裹)不必要的重渲染(如上面的handleClick示例)。
2. Vue:用 v-memo/computed/keep-alive 优化
Vue 的优化思路与 React 类似,核心是 “减少不必要的重渲染和 DOM 操作”。
(1)v-memo:列表渲染缓存
用于v-for列表,缓存列表项,避免列表整体重渲染时未变化的项重新渲染(类似 React 的React.memo+ 列表 key)。
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.value]">
<!-- 仅当item.value变化时,该列表项才重新渲染 -->
{{ item.name }}: {{ item.value }}
</div>
</template>
(2)computed:缓存计算属性
Vue 的computed会自动缓存计算结果,仅当依赖的响应式数据变化时才重新计算,避免模板中重复计算。
<template>
<p>总价格:{{ totalPrice }}</p>
</template>
<script setup>
import { ref, computed } from 'vue';
const goods = ref([
{ price: 100, count: 2 },
{ price: 200, count: 1 }
]);
// computed缓存总价格,仅当goods变化时重新计算
const totalPrice = computed(() => {
console.log('重新计算总价格');
return goods.value.reduce((sum, item) => sum + item.price * item.count, 0);
});
</script>
(3)keep-alive:缓存组件实例
用于缓存路由组件或动态组件,避免组件频繁创建和销毁(如标签页切换、列表页→详情页→返回列表页),节省性能。
<!-- 路由缓存:缓存所有路由组件 -->
<router-view v-slot="{ Component }">
<keep-alive>
<Component :is="Component" />
</keep-alive>
</router-view>
<!-- 动态组件缓存:仅缓存指定组件 -->
<keep-alive include="ComponentA,ComponentB">
<component :is="currentComponent" />
</keep-alive>
3. 通用框架优化:合理使用 key、按需加载组件
- key 优化:列表渲染时,key必须是唯一标识(如 id),不能用index—— 如果用index,列表增删改查时会导致 DOM 节点复用错误,触发不必要的重渲染。
- 按需加载组件库:如 Element Plus、Ant Design Vue,用插件(如unplugin-vue-components)自动导入用到的组件和样式,避免全量引入导致的包体积过大。
五、首屏优化:大厂最关心的 “门面工程”
首屏加载速度直接影响用户留存(研究表明:首屏加载超过 3 秒,用户流失率超过 50%),也是大厂面试的重中之重。以下是首屏优化的核心方案。
1. SSR(服务端渲染):让首屏 “秒出”
CSR(客户端渲染)的问题:浏览器拿到 HTML 后,需要下载 JS、执行 JS、渲染组件,首屏空白时间长;而 SSR(服务端渲染)在服务器端就已渲染好完整的 HTML,浏览器拿到后直接渲染,大幅提升 FCP(首内容绘制)和 LCP(最大内容绘制)。
SSR 核心流程:
- 用户请求页面 → 服务器接收请求;
- 服务器执行 JS(如 Next.js/Nuxt.js),获取数据、渲染组件,生成完整 HTML;
- 服务器将 HTML 发送给浏览器;
- 浏览器直接渲染 HTML(首屏可见),同时下载客户端 JS( hydration 激活交互)。
优势与缺点:
- 优势:首屏快、利于 SEO(搜索引擎能抓取完整 HTML);
- 缺点:服务器压力大、开发复杂度高(需处理服务端 / 客户端环境差异)。
框架实践:
- React:Next.js(主流,支持 SSR/SSG/ISR);
- Vue:Nuxt.js(Vue 官方推荐,用法类似 Next.js)。
2. 骨架屏(Skeleton Screen):提升用户感知
骨架屏是 “首屏加载中的占位 UI”,用灰色块模拟页面结构(如标题、图片、列表),让用户知道 “页面正在加载,不是卡了”,提升感知体验。
实现方式:
- 静态骨架屏:用 HTML+CSS 写固定骨架结构,嵌入到首屏 HTML 中,加载完成后隐藏;
- 动态骨架屏:根据接口数据动态生成骨架结构(如列表有 10 项,生成 10 个列表骨架);
- 组件库集成:Element Plus/Ant Design Vue 自带Skeleton组件,直接使用。
✅ 静态骨架屏示例:
<!-- 骨架屏HTML -->
<div class="skeleton">
<div class="skeleton__title"></div>
<div class="skeleton__image"></div>
<div class="skeleton__list">
<div class="skeleton__list-item"></div>
<div class="skeleton__list-item"></div>
</div>
</div>
<!-- 骨架屏CSS(用动画模拟加载中) -->
<style>
.skeleton { padding: 16px; }
.skeleton__title { width: 50%; height: 24px; background: #f2f2f2; border-radius: 4px; margin-bottom: 16px; }
.skeleton__image { width: 100%; height: 200px; background: #f2f2f2; border-radius: 4px; margin-bottom: 16px; }
.skeleton__list-item { width: 100%; height: 40px; background: #f2f2f2; border-radius: 4px; margin-bottom: 8px; }
/* 动画:渐变效果 */
.skeleton * {
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
3. 其他首屏优化方案
- HTTP/2 Server Push:服务器在发送 HTML 时,主动推送首屏所需的 CSS/JS(如link rel="preload"),避免浏览器等待 HTML 解析完成后再请求资源;
- 代码分割(Code Splitting) :将首屏 JS 拆分为 “公共库”(如 Vue/React,长期缓存)和 “业务代码”(按需加载),减少首屏 JS 体积;
- 服务端缓存:对 SSR 生成的 HTML 进行缓存(如 Redis),相同请求直接返回缓存的 HTML,减少服务器渲染时间。
六、缓存策略:让资源 “一次加载,多次复用”
缓存是性能优化的 “放大器”—— 通过缓存已加载的资源,下次请求时直接复用,避免重复下载,大幅减少加载时间。
1. HTTP 缓存:强缓存 vs 协商缓存(面试核心)
HTTP 缓存是浏览器与服务器之间的缓存机制,分为「强缓存」和「协商缓存」,优先级:强缓存 > 协商缓存。
(1)强缓存:不发请求,直接用本地缓存
浏览器首次请求资源后,服务器返回缓存规则(如Cache-Control),后续请求时,浏览器判断缓存是否过期,未过期则直接使用本地缓存,不发 HTTP 请求。
核心字段:
- Cache-Control(HTTP/1.1,优先级高):
-
- max-age=3600:缓存 3600 秒(1 小时);
-
- public:允许所有缓存(浏览器、CDN 等)缓存;
-
- private:仅允许浏览器缓存;
-
- no-cache:不使用强缓存,直接进入协商缓存;
-
- no-store:不缓存任何资源(每次都请求新资源)。
- Expires(HTTP/1.0,已过时):用绝对时间指定缓存过期时间(如Expires: Wed, 21 Oct 2024 07:28:00 GMT),依赖客户端时间,可能不准,已被Cache-Control替代。
(2)协商缓存:发请求,判断是否复用缓存
强缓存过期后,浏览器会发送请求到服务器,服务器判断资源是否变化:
- 未变化:返回 304 Not Modified,浏览器使用本地缓存;
- 已变化:返回 200 OK 和新资源。
核心字段:
| 服务器响应头(首次请求) | 浏览器请求头(后续请求) | 原理 | 优缺点 |
|---|---|---|---|
| Last-Modified | If-Modified-Since | 基于资源修改时间戳 | 优点:计算简单;缺点:精度到秒,文件内容不变但时间变会误判 |
| ETag | If-None-Match | 基于资源内容哈希值 | 优点:精度高;缺点:计算哈希耗服务器性能 |
HTTP 缓存流程总结:
- 浏览器请求资源 → 检查强缓存(Cache-Control/Expires);
- 强缓存未过期 → 直接使用本地缓存;
- 强缓存过期 → 发送请求到服务器,携带协商缓存字段(If-Modified-Since/If-None-Match);
- 服务器判断资源未变化 → 返回 304,浏览器使用本地缓存;
- 服务器判断资源已变化 → 返回 200 和新资源,更新本地缓存。
2. 客户端缓存:localStorage/sessionStorage/cookie
用于缓存客户端数据(如用户信息、配置项),减少接口请求。
| 特性 | localStorage | sessionStorage | cookie |
|---|---|---|---|
| 存储大小 | 5MB | 5MB | 4KB |
| 有效期 | 永久(除非手动删除) | 会话级(标签页关闭消失) | 可设置过期时间(如 1 天) |
| 作用域 | 同域名下所有标签页 | 仅当前标签页 | 同域名(可设置 path/domain) |
| 通信方式 | 仅客户端,不随请求发送 | 仅客户端,不随请求发送 | 随每次 HTTP 请求发送(可设置 httpOnly) |
| 安全性 | 低(易被 XSS 攻击) | 低(易被 XSS 攻击) | 可设置 httpOnly(防 XSS)、Secure(仅 HTTPS)、SameSite(防 CSRF) |
适用场景:
- localStorage:存储用户偏好(如主题、语言)、长期有效的数据;
- sessionStorage:存储临时表单数据、会话级数据(如验证码);
- cookie:存储身份认证信息(如 token,需设置httpOnly)、跨域请求携带的小数据。
七、性能测试:用工具量化优化效果
优化不能凭感觉,需要用工具量化性能指标,找到瓶颈,验证优化效果。
1. Chrome DevTools Performance:分析渲染和执行瓶颈
使用步骤:
- 打开 Chrome → F12 打开 DevTools → 切换到 Performance 标签;
- 点击「Record」按钮(圆形按钮)→ 操作页面(如刷新、点击)→ 点击「Stop」;
- 分析报告:
-
- Main:主线程任务(JS 执行、Layout、Paint 等),长任务(红色块)会阻塞渲染;
-
- Frames:帧率(正常 60fps,低于 30fps 会卡顿);
-
- Timings:关键指标(FCP、LCP、FID 等);
-
- Call Tree:分析 JS 函数执行时间,找到耗时函数。
核心指标:
- FCP(First Contentful Paint) :首内容绘制,理想值 < 1.8s;
- LCP(Largest Contentful Paint) :最大内容绘制,理想值 < 2.5s(Core Web Vitals 核心指标);
- FID(First Input Delay) :首次输入延迟,理想值 < 100ms(衡量交互响应速度);
- CLS(Cumulative Layout Shift) :累积布局偏移,理想值 < 0.1(衡量布局稳定性,避免页面跳动)。
2. Lighthouse:生成全面性能报告
Lighthouse 是 Chrome 官方性能测试工具,不仅能测性能,还能评估无障碍、SEO、最佳实践等,生成详细优化建议。
使用步骤:
- Chrome DevTools → Lighthouse 标签 → 勾选「Performance」等选项;
- 点击「Generate report」→ 等待测试完成;
- 查看报告:
-
- 性能分数(0~100,80 + 为优秀);
-
- 具体优化建议(如 “压缩图片”“启用 Gzip”“减少 JS 体积”)。
八、面试总结:如何让面试官眼前一亮?
讲性能优化时,别只说 “要做图片懒加载、防抖节流”,这样太简单了。我们想要让面试官眼前一亮,最主要的向让面试官展现出我们基础知识的牢固、全面和你有实践的经历。
所有核心是体现 “理解底层 + 注重实践” —— 既要讲清方案原理,也要说清落地细节和量化效果
AI生成参考回答
- 基础核心(重绘重排) :先讲浏览器渲染流水线,再对比重绘重排定义与区别,接着给出 6 个实战优化方>案(合并样式操作、文档碎片、脱离文档流、缓存布局信息、transform 替代传统样式、避免强制同步布局),每个>方案配代码示例与底层逻辑。
- 资源加载优化:聚焦图片(WebP、懒加载、压缩裁剪)、脚本(async/defer/module 对比)、资源预加载(preload/prefetch/dns-prefetch)等,讲清每种优化的底层逻辑(如 WebP 压缩原理、脚本加载阻塞机制)与实践方式。
- JS 执行优化:围绕主线程不阻塞,讲解防抖节流(手写核心代码 + 适用场景)、Web Workers(子线程原理 + 实战案例)、requestAnimationFrame/requestIdleCallback(动画与空闲任务优化逻辑)。
- 框架层优化:分 React(memo/useMemo/useCallback)与 Vue(v-memo/computed/keep-alive),结合框架特性讲优化逻辑,补充 key 使用、组件按需加载等通用技巧。
- 首屏优化(重点) :针对大厂关注点,讲解 SSR(流程、优缺、框架)、骨架屏(实现方式)、代码分割等方案,强调首屏优化对用户留存的影响。
- 缓存策略:对比 HTTP 缓存(强缓存 vs 协商缓存核心字段与流程)与客户端缓存(localStorage/sessionStorage/cookie 特性与场景),讲清缓存复用逻辑。
- 性能测试工具:介绍 Chrome Performance(分析步骤、核心指标)与 Lighthouse(使用步骤、报告解读),说明工具如何量化优化效果、定位瓶颈。
将性能优化讲的全面、细致。透露出你有实践经历,可以直接上手工作。这样就能让你在面试官眼前一亮。
最后:性能优化的核心思路
性能优化不是 “一蹴而就” 的,而是 “持续迭代” 的过程,核心思路可以总结为 3 点:
- 减少资源体积:压缩 JS/CSS、用 WebP 图片、代码分割;
- 减少请求次数:合并请求、缓存资源、懒加载;
- 优化执行效率:避免重排重绘、减少主线程阻塞、利用 GPU 加速。
记住:性能优化没有 “银弹”,需要结合项目场景(如首屏、列表、动画)选择合适的方案,并用工具量化效果,才能真正提升用户体验。