大家好!在前端面试中,"性能优化" 绝对是高频考点 ------ 毕竟用户体验直接影响产品生死,而性能就是体验的核心。这篇文章会从浏览器底层原理 出发,层层递进讲到框架层优化 和工程化实践,每个知识点都拆解得足够细致
浏览器渲染机制与性能瓶颈
要做性能优化,首先得知道 "性能瓶颈" 在哪。浏览器渲染页面的核心流程叫关键渲染路径(Critical Rendering Path) ,步骤如下:
- 解析 HTML → 生成 DOM 树(处理标签和文本,忽略 CSS/JS)
- 解析 CSS → 生成 CSSOM 树(处理样式,不管 HTML 结构)
- DOM + CSSOM → 生成渲染树 (只包含 "需要显示的节点",比如隐藏的
display: none
元素不会进入渲染树) - 布局(Layout) :计算渲染树中每个节点的尺寸、位置(这一步就是 "重排" 的核心)
- 绘制(Paint) :把节点的样式(颜色、背景等)画到屏幕上(这一步就是 "重绘" 的核心)
- 合成(Composite) :把绘制好的图层合并,最终显示在屏幕上(GPU 加速的关键环节)
❗ 关键点:重排必然导致重绘,但重绘不一定导致重排。比如改颜色(只重绘),改宽度(重排 + 重绘)。优化的核心就是 "减少重排重绘次数" 和 "降低其耗时"。
基础优化:跟重排重绘说 "再见"👋
这部分是面试必问,必须吃透!我们先明确概念,再讲具体解决方案。
1. 重绘 vs 重排:先分清两者的区别
类型 | 触发场景 | 性能影响 |
---|---|---|
重绘(Repaint) | 样式改变但不影响布局,比如:color 、background 、box-shadow |
中(只需要重新绘制像素) |
重排(Reflow/Layout) | 布局改变,比如:width 、height 、margin 、offsetTop 、增删 DOM 节点 |
高(需要重新计算布局,影响其他节点) |
2. 如何减少重排重绘?6 个实用技巧
技巧 1:批量修改 DOM(避免 "单次修改多次触发")
浏览器会有 "渲染队列" 优化,合并多次 DOM 操作,但手动批量处理更稳妥,避免意外触发重排。
js
// ❌ 错误:多次修改style,可能触发多次重排(即使浏览器合并,也不推荐)
const el = document.querySelector('.box');
el.style.width = '200px';
el.style.height = '200px';
el.style.margin = '20px';
// ✅ 正确方式1:用cssText批量设置
el.style.cssText = 'width:200px; height:200px; margin:20px;';
// ✅ 正确方式2:用className(更易维护,推荐!)
.el-active {
width: 200px;
height: 200px;
margin: 20px;
}
el.className = 'el-active';
技巧 2:用文档碎片(DocumentFragment)批量添加元素
如果要循环创建多个 DOM 节点(比如列表),直接appendChild
会每次都触发重排,而DocumentFragment
是 "虚拟 DOM 容器",不会插入真实 DOM,最后一次性添加才触发 1 次重排。
js
// ✅ 优化方案:文档碎片
const fragment = document.createDocumentFragment(); // 虚拟容器
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `列表项${i}`;
fragment.appendChild(li); // 操作虚拟容器,不触发重排
}
document.querySelector('ul').appendChild(fragment); // 只触发1次重排
技巧 3:让元素 "脱离文档流" 再操作
元素脱离文档流后,它的修改不会影响其他节点的布局,自然不会触发重排。常用两种方式:
position: absolute/fixed
:脱离普通流,布局计算独立display: none
:直接从渲染树中移除,操作完再恢复
js
const el = document.querySelector('.box');
// 1. 先脱离文档流
el.style.position = 'absolute'; // 或 el.style.display = 'none';
// 2. 执行大量修改(此时不触发重排)
el.style.width = '300px';
el.style.height = '300px';
el.style.background = 'red';
// 3. 恢复文档流
el.style.position = 'static'; // 或 el.style.display = 'block';
技巧 4:缓存布局信息(避免 "读取 - 修改" 循环)
有些属性(比如offsetTop
、scrollTop
、clientWidth
)属于 "布局属性"------每次读取都会强制浏览器刷新渲染队列,触发重排。如果在循环中反复读取 + 修改,会导致 "重排风暴"。
解决办法:先一次性读取并缓存,再批量修改。
js
// ❌ 错误:循环中反复读取offsetTop,触发100次重排
const el = document.querySelector('.box');
for (let i = 0; i < 100; i++) {
el.style.top = el.offsetTop + 1 + 'px'; // 每次都读offsetTop,触发重排
}
// ✅ 正确:先缓存布局信息,再修改
let top = el.offsetTop; // 只读取1次,缓存值
for (let i = 0; i < 100; i++) {
top++; // 操作缓存值,不触发重排
}
el.style.top = top + 'px'; // 只修改1次,触发1次重排
技巧 5:用 transform 代替传统位置修改
left
、top
修改会触发重排,但transform
属于 "合成层操作"------ 浏览器会为元素创建独立的合成层,通过 GPU 加速渲染,只触发合成,不触发重排和重绘,性能提升巨大!
js
// ❌ 传统方式:触发重排+重绘
el.style.left = '100px';
el.style.top = '50px';
// ✅ 优化方式:只触发合成,性能更好
el.style.transform = 'translateX(100px) translateY(50px)';
✨ 补充:可以用
will-change: transform
提前告诉浏览器 "这个元素要动",让浏览器提前做好优化准备(但别滥用,否则会占用过多 GPU 资源)。
性能测试
光说不练假把式,实际开发中要先定位重排重绘问题,用 Chrome DevTools:
- 打开 F12 → 切换到 Performance 面板
- 点击 "录制" 按钮,操作页面
- 录制结束后,查看 "Main" 线程中的 "Layout"(重排)和 "Paint"(重绘)事件,红色块越多说明性能越差。

Lighthouse 是 Chrome 自带的开源性能测试工具,会从 性能、无障碍、最佳实践、SEO 四个维度对页面打分(满分 100),并生成详细的问题报告和优化建议。
核心性能指标(Lighthouse 重点关注):
- LCP(最大内容绘制) :衡量首屏加载速度,目标 < 2.5s。
- FID(首次输入延迟) :衡量交互响应速度,目标 < 100ms(已被 INP 替代)。
- INP(交互下一步延迟) :衡量页面交互流畅度,目标 < 200ms。
- CLS(累积布局偏移) :衡量页面布局稳定性,目标 < 0.1。

Performance API:自定义性能埋点
除了工具测试,还可以用浏览器原生的 Performance API 自定义埋点,监控特定操作的性能(如 DOM 渲染时间、接口请求时间)。
示例:监控列表首个元素的渲染时间
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance API Example</title>
<style>
#myList{
margin:20px;
padding:0;
list-style-type:none;
}
</style>
</head>
<body>
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<script>
document.addEventListener('DOMContentLoaded',function(){
// 1. 创建性能标记(mark):标记关键时间点
performance.mark('start'); // 开始标记
// 2. 执行目标操作:获取并验证首个列表元素
const firstLi=document.querySelector('#myList li');
if(firstLi){
performance.mark('firstLiRendered'); // 首个元素渲染完成标记
}
performance.mark('end'); // 结束标记
// 3. 计算两个标记之间的耗时(measure)
performance.measure(
'First LI Render Time', // 测量名称
'start', // 开始标记
'firstLiRendered' // 结束标记
);
// 4. 获取并打印测量结果
const measureResult=performance.getEntriesByName('First LI Render Time');
if(measureResult.length>0){
console.log('首个列表元素渲染时间:',measureResult[0].duration.toFixed(2),'ms');
}
// 5. 清除标记和测量结果(可选,释放内存)
performance.clearMarks();
performance.clearMeasures();
})
</script>
</body>
</html>
核心 API 说明:
performance.mark(name)
:创建一个性能标记,记录当前时间点。performance.measure(measureName, startMark, endMark)
:计算两个标记之间的耗时。performance.getEntriesByName(name)
:获取指定名称的标记或测量结果。performance.clearMarks()
/performance.clearMeasures()
:清除标记和测量结果,释放内存。
资源加载优化:让页面 "快" 到飞起⚡
资源加载是首屏慢的主要原因之一,这部分要覆盖 "图片、JS、CSS" 等所有资源类型,面试常考细节!
1. 图片优化:占比最大的资源,必须优化
图片是页面中体积最大的资源,优化好图片能直接降低 50% 以上的加载时间。
(1)选择合适的图片格式
格式 | 特点 | 适用场景 |
---|---|---|
WebP | 体积比 JPG 小 30%,比 PNG 小 50%,支持透明和动图 | 绝大多数场景(兼容性:IE 不支持,可降级) |
AVIF | 比 WebP 更小(再小 20%),兼容性稍差 | 追求极致体积,且用户设备支持(现代浏览器) |
SVG | 矢量图,放大不失真,体积小 | 图标、Logo、简单图形(避免用 SVG 做复杂图片) |
JPG | 有损压缩,体积小,不支持透明 | 照片、复杂图片(比如商品图、文章封面) |
PNG | 无损压缩,支持透明,体积大 | 透明背景图、图标(小尺寸) |
❗ 面试考点:如何实现 WebP 降级?
用
<picture>
标签,浏览器会自动选择支持的格式:
html<picture> <source srcset="image.avif" type="image/avif"> <source srcset="image.webp" type="image/webp"> <img src="image.jpg" alt=" fallback"> <!-- 不支持时显示JPG --> </picture>
(2)图片懒加载:"看得见再加载"
默认情况下,浏览器会加载页面所有图片,包括屏幕外的。懒加载只加载 "进入视口" 的图片,减少
有 3 种实现方式,按推荐度排序:
-
原生懒加载 (最简单,推荐):给
<img>
加loading="lazy"
属性(兼容性:Chrome 77+,Edge 79+)html<img src="image.jpg" alt="..." loading="lazy" width="400" height="300">
-
IntersectionObserver API (现代方案,性能好):监听图片是否进入视口,进入后再设置
src
jsconst observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // 图片进入视口 const img = entry.target; img.src = img.dataset.src; // 从data-src取真实地址 observer.unobserve(img); // 加载后停止监听 } }); }); // 给所有懒加载图片加监听 document.querySelectorAll('img[data-src]').forEach(img => { observer.observe(img); });
-
传统 scroll 监听 (兼容性好,性能差):监听
scroll
事件,判断图片位置,缺点是频繁触发事件,需配合节流。
(3)其他图片优化技巧
- 压缩图片:用工具(TinyPNG、Squoosh)压缩,保留质量的同时减小体积
- 设置正确尺寸:避免 "图片实际尺寸 1000px,页面显示 200px",浪费带宽
- 用图标字体库:比如 Font Awesome、Iconfont,1 个字体文件替代多个图标图片,减少 HTTP 请求(还能通过
font-size
随意调整大小)
2. JS 资源加载:避免阻塞渲染
JS 默认会 "阻塞 DOM 解析" 和 "阻塞渲染"------ 浏览器遇到<script>
标签会暂停 HTML 解析,先下载并执行 JS,这会导致首屏渲染延迟。
解决办法:用async
、defer
、type="module"
控制 JS 加载执行顺序。
属性 | 加载方式 | 执行时机 | 适用场景 |
---|---|---|---|
无属性 | 同步加载 | 下载完立即执行,阻塞 DOM 解析 | 首屏必需的 JS(比如骨架屏、核心交互) |
async | 异步加载 | 下载完立即执行,顺序不保证 | 独立的第三方脚本(比如统计、广告) |
defer | 异步加载 | 所有 JS 下载完后,按顺序执行,等待 DOMContentLoaded | 依赖顺序的脚本(比如 A.js 依赖 B.js) |
type="module" | 异步加载(默认 defer) | 按顺序执行,支持 ES6 模块 | 现代项目的 JS 模块(比如 Vue/React 组件) |
示例代码:
html
<!-- 1. 首屏必需JS:同步加载(放body底部,避免阻塞首屏解析) -->
<script src="core.js"></script>
<!-- 2. 第三方统计脚本:async,加载完就执行 -->
<script src="analytics.js" async></script>
<!-- 3. 依赖顺序的脚本:defer,按顺序执行 -->
<script src="B.js" defer></script>
<script src="A.js" defer></script> <!-- A依赖B,会在B之后执行 -->
<!-- 4. ES6模块:默认defer,支持import/export -->
<script type="module" src="app.js"></script>
3. CSS 资源加载:关键 CSS 内联
CSS 会 "阻塞渲染"(浏览器要等 CSSOM 构建完才能生成渲染树),但不会阻塞 DOM 解析。优化技巧:
-
内联关键 CSS :把首屏必需的 CSS(比如导航、Banner 样式)内联到
<head>
的<style>
标签中,避免外部 CSS 文件加载延迟导致的 "白屏"。 -
异步加载非关键 CSS :非首屏 CSS(比如页脚、详情页样式)用
rel="preload"
异步加载,加载完再应用。html<!-- 1. 内联关键CSS:首屏立即渲染 --> <head> <style> .header { height: 60px; background: #fff; } .banner { width: 100%; height: 300px; } </style> <!-- 2. 异步加载非关键CSS --> <link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> </head>
4. 资源预加载 / 预解析:提前 "备货"
如果知道用户下一步会操作什么(比如点击 "下一页"),可以提前加载资源,让用户操作时 "秒开"。
类型 | 用法 | 适用场景 |
---|---|---|
preload | <link rel="preload" href="font.woff2" as="font"> |
当前页面必需的资源(比如字体、关键 JS) |
prefetch | <link rel="prefetch" href="next-page.js"> |
下一页可能用到的资源(比如分页的下一页 JS) |
dns-prefetch | <link rel="dns-prefetch" href="//cdn.example.com"> |
提前解析域名 DNS(减少 DNS 查询时间,比如 CDN 域名) |
preconnect | <link rel="preconnect" href="//cdn.example.com"> |
提前建立 TCP 连接(跳过 DNS 解析、TCP 握手、TLS 协商,比 dns-prefetch 更彻底) |
✨ 注意:preload 不要滥用,否则会占用带宽,反而影响首屏加载。
JS 执行优化:避免 "卡顿",提升交互体验
JS 执行阻塞主线程(包括渲染、交互),如果 JS 执行时间过长,会导致页面卡顿、点击无响应。这部分优化直接影响用户交互体验!
1. 防抖(Debounce):避免 "频繁触发"
场景:搜索输入联想、窗口resize
、滚动加载。
原理:触发事件后,等待 N 秒再执行回调,如果 N 秒内再次触发,重新计时。
js
// 防抖函数(面试常考手写)
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer); // 每次触发都清除定时器,重新计时
timer = setTimeout(() => {
fn.apply(this, args); // 延迟后执行
}, delay);
};
}
// 使用:搜索输入联想
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce((e) => {
console.log('发送搜索请求:', e.target.value);
}, 500));
2. 节流(Throttle):控制 "触发频率"
场景:滚动加载数据、高频点击按钮、鼠标移动事件。
原理:触发事件后,N 秒内只执行 1 次回调,即使多次触发也无效。
js
// 节流函数(面试常考手写,时间戳版)
function throttle(fn, interval = 300) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) { // 间隔超过N秒才执行
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用:滚动加载
window.addEventListener('scroll', throttle(() => {
console.log('滚动加载数据');
}, 1000));
❗ 面试考点:防抖和节流的区别?
- 防抖:"等停止触发后再执行",适合 "输入联想" 等需要等待用户操作结束的场景。
- 节流:"按固定频率执行",适合 "滚动加载" 等需要持续响应的场景。
3. Web Workers:让复杂计算 "不阻塞主线程"
JS 是单线程的,复杂计算(比如大数据排序、Excel 解析、图片处理)会占用主线程,导致页面卡顿。Web Workers 可以创建 "子线程",在子线程中执行复杂计算,不影响主线程。
用法步骤:
-
创建 Worker 文件(worker.js):
js// 子线程:接收主线程消息,执行计算,返回结果 self.onmessage = function(e) { const { data } = e; const result = heavyCompute(data); // 复杂计算函数 self.postMessage(result); // 把结果发给主线程 }; // 复杂计算:比如100万个数排序 function heavyCompute(arr) { return arr.sort((a, b) => a - b); }
-
主线程中使用 Worker:
js// 主线程:创建Worker,发送数据,接收结果 const worker = new Worker('worker.js'); // 发送数据给子线程 const bigData = Array.from({ length: 1000000 }, () => Math.random()); worker.postMessage(bigData); // 接收子线程返回的结果 worker.onmessage = function(e) { console.log('计算结果:', e.data); worker.terminate(); // 计算完成,关闭Worker,释放资源 }; // 监听错误 worker.onerror = function(error) { console.error('Worker错误:', error); };
注意点:
- Worker 不能操作 DOM(比如
document
、window
),只能处理计算逻辑。 - Worker 和主线程通过
postMessage
通信,数据是 "拷贝" 而非 "共享"(大数据会有性能损耗,可改用Transferable Objects
共享数据)。
4. 合理使用 requestAnimationFrame 和 requestIdleCallback
这两个 API 是浏览器提供的 "调度工具",帮助我们在合适的时机执行代码,减少性能损耗。
API | 执行时机 | 适用场景 |
---|---|---|
requestAnimationFrame(rAF) | 与浏览器重绘频率同步(约 60fps,即 16.67ms 执行一次) | 动画效果(比如平滑滚动、进度条),避免掉帧 |
requestIdleCallback(rIC) | 浏览器空闲时执行(执行时机不确定) | 非紧急任务(比如日志上报、数据统计、非关键 DOM 更新) |
示例:用 rAF 做平滑动画
js
// 传统setTimeout做动画:时间不准,可能掉帧
let left = 0;
function animate() {
left++;
el.style.left = left + 'px';
setTimeout(animate, 16); // 16ms约等于60fps,但时间不准
}
// ✅ 用rAF:与重绘同步,更流畅
function animate() {
left++;
el.style.left = left + 'px';
requestAnimationFrame(animate); // 自动同步重绘频率
}
requestAnimationFrame(animate);
框架层优化:React/Vue 开发者必看📚
如果用框架开发,除了基础优化,还要掌握框架专属的优化技巧!
1. React 优化:避免不必要的渲染
React 组件默认会 "父组件渲染,子组件也跟着渲染",即使子组件 props 没变化。优化核心是 "减少无效渲染"。
(1)memo:缓存组件,避免 props 不变时重渲染
用React.memo
包装函数组件,当 props 浅比较不变时,组件不会重渲染。
jsx
// 子组件:用memo包装
const Child = React.memo(({ name }) => {
console.log('Child渲染了'); // props不变时,不会打印
return <div>{name}</div>;
});
// 父组件:name不变时,Child不会重渲染
const Parent = () => {
const [count, setCount] = useState(0);
const name = '小明'; // 不变的props
return (
<div>
<button onClick={() => setCount(count + 1)}>点击{count}</button>
<Child name={name} />
</div>
);
};
(2)useCallback:缓存函数,避免 props 因函数重新创建而变化
函数组件每次渲染都会重新创建内部函数,导致子组件接收的函数 props 变化,即使memo
也会重渲染。useCallback
可以缓存函数引用。
jsx
const Parent = () => {
const [count, setCount] = useState(0);
const name = '小明';
// ✅ 用useCallback缓存函数,依赖不变时,函数引用不变
const handleClick = useCallback(() => {
console.log('点击子组件', name);
}, [name]); // 依赖:name不变,函数不变
return (
<div>
<button onClick={() => setCount(count + 1)}>点击{count}</button>
<Child name={name} onClick={handleClick} />
</div>
);
};
(3)useMemo:缓存计算结果,避免重复计算
如果组件中有复杂计算,每次渲染都会重新计算,useMemo
可以缓存计算结果,依赖不变时直接复用。
jsx
const Parent = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(10);
// ✅ 用useMemo缓存计算结果,依赖num不变时,不重新计算
const total = useMemo(() => {
console.log('执行复杂计算'); // num不变时,不会打印
return num * 100; // 复杂计算
}, [num]);
return (
<div>
<button onClick={() => setCount(count + 1)}>点击{count}</button>
<div>计算结果:{total}</div>
</div>
);
};
2. Vue 优化:类似 React,核心是 "减少无效渲染"
- v-memo:类似 React.memo,缓存组件,避免 props 不变时重渲染。
- computed:缓存计算结果,类似 useMemo,避免重复计算。
- v-for 避免同时用 v-if:v-for 优先级高于 v-if,会先循环再过滤,浪费性能,建议先过滤数据再循环。
3. 组件库按需加载:避免 "加载整个库"
比如 Element Plus、Ant Design Vue、shadcn-ui 等组件库,默认引入会加载所有组件和样式,体积很大。按需加载只加载用到的组件,减少 JS/CSS 体积。
以 shadcn-ui 为例(按需加载):
jsx
// 只引入需要的组件,不引入整个库
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
以 Element Plus 为例(用插件按需加载):
js
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { ElButton, ElInput } from 'element-plus'; // 只引入需要的组件
import 'element-plus/dist/index.css'; // 或按需引入样式
const app = createApp(App);
app.use(ElButton).use(ElInput).mount('#app');
4. 列表渲染优化:合理使用 key
框架(React/Vue)在更新列表时,会通过key
识别 DOM 节点的唯一性,避免不必要的 DOM 复用和重绘重排。
-
❌ 错误:用 index 做 key(列表增删时,index 会变化,导致框架误判节点,引发重排)
jsx// 错误示例:index做key {list.map((item, index) => ( <Item key={index} data={item} /> ))}
-
✅ 正确:用唯一 ID 做 key(比如后端返回的 id,确保不变且唯一)
jsx// 正确示例:唯一ID做key {list.map(item => ( <Item key={item.id} data={item} /> ))}
缓存策略:让资源 "二次加载" 秒开💨
缓存是性能优化的 "利器"------ 第一次加载资源后,第二次直接从缓存读取,无需重新请求服务器,速度极快。面试常考 "HTTP 缓存" 和 "前端存储缓存"。
1. HTTP 缓存:分为 "强缓存" 和 "协商缓存"
HTTP 缓存是浏览器和服务器之间的缓存机制,通过 HTTP 头控制,优先级:强缓存 > 协商缓存。
(1)强缓存:不发请求,直接从缓存读取
浏览器第一次加载资源后,会把资源缓存到本地,下次请求时先看强缓存是否过期,没过期直接用缓存。
控制强缓存的 HTTP 头:
-
Cache-Control (推荐,优先级高于 Expires):用
max-age
指定缓存有效期(单位:秒)Cache-Control: public, max-age=31536000
:公开缓存,有效期 1 年(适合静态资源,比如图片、JS、CSS)Cache-Control: private
:只能客户端缓存,不能被 CDN 等中间节点缓存(适合用户相关数据)Cache-Control: no-cache
:不使用强缓存,直接走协商缓存Cache-Control: no-store
:完全不缓存,每次都请求服务器
-
Expires(旧方案,不推荐):用绝对时间指定缓存过期时间,缺点是依赖客户端时间,如果客户端时间不准会导致缓存失效。
Expires: Wed, 25 Aug 2026 12:00:00 GMT
(2)协商缓存:发请求问服务器 "资源是否更新"
强缓存过期后,浏览器会发送请求到服务器,携带 "资源标识",服务器判断资源是否更新:
-
如果没更新:返回 304 状态码,浏览器用本地缓存(不返回资源体,节省带宽)
-
如果已更新:返回 200 状态码和新资源(更新本地缓存)
控制协商缓存的 HTTP 头(两组搭配使用):
客户端请求头 | 服务器响应头 | 原理 | 缺点 |
---|---|---|---|
If-Modified-Since | Last-Modified | 基于 "资源修改时间戳" 判断 | 1. 时间戳精度低(秒级),1 秒内修改多次无法识别;2. 只改内容不改时间戳,会误判为未更新 |
If-None-Match | ETag | 基于 "资源内容哈希值" 判断(内容变了哈希就变) | 1. 计算哈希值需要服务器消耗资源;2. 不同服务器计算哈希的方式可能不同(比如集群环境) |
❗ 面试考点:HTTP 缓存的完整流程?
浏览器请求资源,先查强缓存(Cache-Control/Expires):
- 未过期:直接用本地缓存(200 OK (from memory cache/disk cache))
- 已过期:进入协商缓存
发送请求到服务器,携带协商缓存头(If-Modified-Since/If-None-Match):
- 服务器判断资源未更新:返回 304,浏览器用本地缓存
- 服务器判断资源已更新:返回 200 和新资源,更新本地缓存
(3)HTTP 缓存最佳实践
- 静态资源(图片、JS、CSS):用强缓存(Cache-Control: max-age=31536000)+ 协商缓存(ETag),并加 "版本号" 或 "哈希值"(比如
app.[hash].js
),更新时改哈希值,强制浏览器加载新资源。 - 动态资源(API 接口):用协商缓存(Last-Modified/ETag),避免强缓存(Cache-Control: no-cache),确保数据实时性。
2. 前端存储缓存:localStorage/sessionStorage/cookie
前端存储用于缓存 "小规模数据"(比如用户信息、配置项),减少 API 请求。
特性 | localStorage | sessionStorage | cookie |
---|---|---|---|
存储大小 | 5-10MB | 5-10MB | 4KB |
有效期 | 永久(除非手动删除) | 会话结束(关闭浏览器 / 标签页) | 可设置 expires/max-age(默认会话级) |
发送方式 | 不随 HTTP 请求发送 | 不随 HTTP 请求发送 | 随每次 HTTP 请求发送(包括跨域请求) |
适用场景 | 持久化数据(比如用户偏好设置) | 临时数据(比如表单草稿、页面跳转参数) | 身份标识(比如 token、sessionId) |
✨ 注意:cookie 存储敏感数据(比如 token)时,要加
HttpOnly
(防止 JS 读取,避免 XSS 攻击)和Secure
(只在 HTTPS 下传输)属性:
httpSet-Cookie: token=xxx; HttpOnly; Secure; SameSite=Lax;
网络优化:让资源 "走最快的路" 到达用户🌐
网络是连接用户和服务器的 "桥梁",网络优化能直接减少资源加载的 "路上时间"。
1. CDN 加速:让资源 "离用户更近"
CDN(内容分发网络)是遍布全球的 "缓存服务器集群",核心是 "就近访问":
- 用户请求静态资源(图片、JS、CSS)时,会先访问最近的 CDN 节点。
- 如果 CDN 节点有缓存,直接返回资源;没有则从源服务器拉取并缓存,下次再用。
CDN 优化技巧:
- 多域名 CDN :浏览器对同一域名的并发请求数有限制(比如 Chrome 默认 6 个),用多域名 CDN(比如
img1.xxx.com
、img2.xxx.com
)可以突破限制,并行加载更多资源。 - CDN 缓存配置:对静态资源设置长缓存(比如 1 年),配合版本号 / 哈希值更新资源。
适用场景:
所有静态资源(图片、JS、CSS、字体文件),不适合动态资源(比如 API 接口返回的 JSON 数据)。
2. 协议优化:用更高效的 HTTP 协议
- HTTP/2:相比 HTTP/1.1,核心优势是 "多路复用"------ 同一个 TCP 连接可以并行传输多个请求 / 响应,避免 "队头阻塞"(HTTP/1.1 同一连接下,一个请求阻塞导致后续请求排队)。
- HTTP/3:基于 QUIC 协议(UDP 协议的改进版),解决 HTTP/2 在弱网下的队头阻塞问题,握手更快(1 次 RTT 即可建立连接),适合移动网络。
- HTTPS:虽然 HTTPS 第一次握手会增加 1-2 个 RTT 的时间,但可以通过 "TLS 会话复用" 和 "HTTP/2" 优化,且 HTTPS 能提升用户信任度,避免中间人攻击。
3. 其他网络优化技巧
- Gzip/Brotli 压缩:服务器对文本资源(HTML、CSS、JS、JSON)进行压缩,减少资源体积(Gzip 压缩率约 60%-70%,Brotli 比 Gzip 高 10%-20%)。
- DNS 预解析 :用
<link rel="dns-prefetch" href="//cdn.xxx.com">
提前解析 CDN 域名的 DNS,减少 DNS 查询时间(DNS 查询通常需要 20-100ms)。 - 减少 TCP 握手次数:用 "TCP 快速打开(TFO)" 和 "连接复用",避免每次请求都重新建立 TCP 连接(TCP 三次握手需要 1 个 RTT 时间)。
首屏优化:用户 "第一眼" 的体验至关重要👀
首屏加载时间是用户体验的 "第一印象",面试中经常会问 "如何优化首屏加载速度",这里总结 6 个核心方案。
1. SSR(服务端渲染):让浏览器 "直接显示" 页面
传统 SPA(单页应用)是 "浏览器加载 JS → JS 渲染页面",首屏会有 "白屏";SSR 是 "服务器渲染页面 HTML → 浏览器直接显示 HTML",首屏加载更快。
原理:
- 用户请求页面 → 服务器接收请求。
- 服务器执行 JS(比如 React/Vue 组件),渲染出完整的 HTML。
- 服务器把 HTML 返回给浏览器 → 浏览器直接显示页面(无需等待 JS 加载执行)。
- 浏览器加载 JS,激活页面交互(此时页面已显示,用户可以浏览)。
适用场景:
首屏要求高的网站(比如电商首页、新闻网站),缺点是服务器压力大,开发复杂度高。
2. 骨架屏:让用户 "知道页面在加载"
骨架屏是 "页面加载中的占位 UI"(比如灰色的标题、图片占位块),替代白屏,让用户感知 "页面正在加载",减少等待焦虑。
实现方式:
- 静态骨架屏:直接在 HTML 中写骨架屏的 CSS 和 HTML(首屏立即显示)。
- 动态骨架屏:用 JS 根据页面结构生成骨架屏(适合复杂页面)。
3. 其他首屏优化方案
- 首屏资源优先级 :用
preload
加载首屏必需的资源(比如关键 JS、CSS、首屏图片),提升加载优先级。 - 减少首屏请求数:内联关键 CSS/JS、合并资源、用图标字体替代图片图标。
- 预渲染(Prerendering) :针对静态页面(比如营销页),构建时提前渲染页面 HTML,比 SSR 简单,无需服务器实时渲染。
- HTTP/2 Server Push:服务器在用户请求 HTML 时,主动推送首屏必需的资源(比如 CSS、JS),避免浏览器等待 HTML 解析完再请求资源,减少加载时间。
性能的关键指标:量化用户体验
性能优化的目标是 "提升用户体验",而体验需要通过可量化的指标来衡量。以下是 Web 性能的核心指标,也是面试高频考点。
1. FCP(First Contentful Paint):首次内容绘制
- 定义:衡量浏览器首次渲染出页面 "有意义内容"(如文本、图片、非白色背景)的时间,标志着用户从 "白屏" 到 "看到内容" 的转变。
- 计算逻辑 :从页面开始加载(
navigationStart
)到首次有内容元素渲染完成的时间差。 - 目标值:优秀 <1.8s,良好 < 3s,需优化> 3s。
- 影响因素:首屏资源加载速度(如 HTML、关键 CSS)、服务器响应时间。
2. LCP(Largest Contentful Paint):最大内容绘制
- 定义 :衡量页面中 "最大可见内容元素"(如大图片、标题文本块)完全渲染完成的时间,是评估首屏加载性能的核心指标。
- 计算逻辑:从页面开始加载到最大内容元素渲染完成的时间差(元素需满足 "可见" 且 "尺寸最大")。
- 目标值:优秀 <2.5s,良好 < 4s,需优化> 4s。
- 影响因素:最大内容元素的大小、加载速度(如图片是否压缩、是否懒加载)、服务器响应时间。
✨ 注意:LCP 元素可能是图片、视频、文本块,需确保其加载优先级(如用
preload
预加载大图片)。
3. 其他关键指标(补充)
指标名称 | 定义 | 目标值 | 核心作用 |
---|---|---|---|
INP(Interaction to Next Paint) | 衡量页面交互的 "流畅度",计算用户操作(点击、滚动等)到下一次页面重绘的延迟,取所有交互延迟的第 90 百分位值。 | < 200ms | 评估用户交互体验(如按钮点击是否卡顿) |
CLS(Cumulative Layout Shift) | 衡量页面布局的 "稳定性",计算页面元素意外偏移的累积值(如图片加载后挤压文本)。 | < 0.1 | 避免用户操作被 "突然偏移的元素" 干扰(如误触) |
FID(First Input Delay) | 衡量首次用户交互的 "响应速度",计算用户首次操作到浏览器开始处理事件的延迟(已被 INP 替代)。 | < 100ms | 评估页面 "可交互" 的速度 |
指标获取方式
-
Lighthouse:自动计算 FCP、LCP、INP、CLS 等指标,并给出评分。
-
Chrome DevTools Performance 面板:在 "Timings" 模块中查看 FCP、LCP 时间。
-
Web Vitals API :浏览器原生 API,可在代码中监听并上报指标数据:
js// 监听 LCP 指标 new PerformanceObserver((entryList) => { const lcpEntry = entryList.getEntries()[0]; console.log('LCP 时间:', lcpEntry.startTime.toFixed(2), 'ms'); }).observe({ type: 'largest-contentful-paint', buffered: true });
总结
前端性能优化是一个 "从底层到上层" 的系统工程,核心逻辑可以总结为三点:
- 减少资源体积:压缩图片、JS、CSS,选择合适的格式(如 WebP),拆分代码(Code Splitting)。
- 加快资源加载:用 CDN 加速、HTTP 缓存、预加载,优化网络协议(HTTP/2/3)。
- 减少渲染阻塞:避免重排重绘,用 GPU 加速(transform),优化 JS 执行(防抖、节流、Web Workers)。
掌握这些技巧不仅能应对面试,更能在实际项目中提升用户体验 ------ 毕竟,"快" 就是最好的体验之一。