在秋招面试中经常会遇到面试官问:"你的项目中有哪些性能优化、你知道的性能优化有哪些?" 本篇文章小编将给大家讲讲性能优化这个话题(如有没涉及到的欢迎在评论区补充)
一、渲染性能基石:理解重排(Reflow)与重绘(Repaint)
浏览器将 HTML、CSS 和 JavaScript 转化为用户可见的页面,需要经过 解析 -> 样式计算 -> 布局(重排) -> 绘制(重绘) -> 合成
的流程。其中,重排 和重绘是性能消耗的重灾区。
1. 核心概念辨析
概念 | 触发条件 | 性能开销 | 关系 |
---|---|---|---|
重排 (Reflow/ Layout) | 元素的几何属性 改变(width , height , margin , padding , display: none , offsetTop 读取等)。 |
极高 | 重排必然导致后续的重绘。 |
重绘 (Repaint) | 元素的外观 改变,但不影响布局(color , background-color , visibility )。 |
中等 | 重绘不会导致重排。 |
关键认知:避免不必要的重排是性能优化的首要任务。
2. 优化策略与实战
策略 1:批量修改 DOM 属性
javascript
// ❌ 反模式:连续修改可能触发多次重排
const el = document.getElementById('box');
el.style.width = '200px'; // 可能触发重排
el.style.height = '150px'; // 可能再次触发重排
el.style.margin = '20px'; // 可能再次触发重排
// ✅ 推荐 1:使用 `cssText` 批量设置
el.style.cssText = 'width: 200px; height: 150px; margin: 20px;';
// ✅ 推荐 2:通过切换 CSS 类名(最佳)
el.className = 'new-style'; // 样式定义在 CSS 文件中
原理:CSS 类名变更由浏览器在样式计算阶段统一处理,通常只会触发一次重排。
策略 2:使用文档片段(DocumentFragment)
javascript
// ✅ 批量添加子元素
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const child = document.createElement('div');
child.textContent = `Item ${i}`;
fragment.appendChild(child); // 在内存中操作,无重排
}
// 将片段一次性插入 DOM,触发一次重排
document.getElementById('container').appendChild(fragment);
策略 3:脱离文档流操作("下线"优化)
javascript
const el = document.getElementById('complex-el');
// 1. 脱离文档流,避免操作时触发重排
el.style.display = 'none'; // 或 position: absolute
// 2. 在"下线"状态下进行大量复杂操作
for (let i = 0; i < 100; i++) {
el.style.left = i * 10 + 'px';
el.style.top = i * 5 + 'px';
// ... 其他操作
}
// 3. 操作完成,重新"上线"
el.style.display = 'block';
// 此时触发一次重排和重绘
策略 4:缓存布局信息
javascript
// ❌ 危险:每次读取 offsetTop 都可能强制触发重排
for (let i = 0; i < 100; i++) {
el.style.top = el.offsetTop + 1 + 'px'; // 每次都重排!
}
// ✅ 安全:缓存初始值
const initialTop = el.offsetTop; // 读取一次,触发一次重排
for (let i = 0; i < 100; i++) {
el.style.top = initialTop + i + 'px'; // 仅修改,不读取布局
}
策略 5:使用 transform
代替位置属性
javascript
// ❌ 触发重排(改变几何属性)
el.style.left = '100px';
// ✅ 仅触发重绘(或合成),性能极佳
el.style.transform = 'translateX(100px)';
原理 :
transform
属于合成(Compositing)层操作,通常由 GPU 加速,不触发主线程的重排和重绘。
📌 面试环节:重排与重绘
Q1: 请解释什么是重排(Reflow)和重绘(Repaint)?它们有什么区别和联系?
A: 重排是当 DOM 元素的几何属性(如宽高、位置)发生变化时,浏览器需要重新计算元素的布局和位置,这个过程开销很大。重绘是当元素的外观(如颜色、背景)改变但不影响布局时,浏览器需要重新绘制该元素。重排一定会导致重绘,因为布局改变后外观也需要更新;但重绘不一定导致重排。
Q2: 如何避免或减少重排?请举例说明。
A: 主要策略有:
- 批量操作 :使用
cssText
或切换className
一次性修改样式。- 文档片段 :使用
DocumentFragment
批量添加节点。- 脱离文档流 :操作前将元素
display: none
或position: absolute
,操作完再恢复。- 缓存布局信息 :避免在循环中读取
offsetTop
等布局属性。- 使用
transform
:用transform
代替left/top
等位置属性进行动画。
Q3: 为什么 transform
比 left/top
性能更好?
A :
left/top
会修改元素的几何属性,触发重排和重绘。而transform
属于合成层操作,通常由 GPU 处理,不触发主线程的重排和重绘,只可能触发合成(Compositing),性能开销小得多。
二、资源加载优化:减少等待时间
资源加载是影响首屏时间(FCP, LCP)的关键。
1. 图片优化
- 懒加载 (Lazy Loading) :
loading="lazy"
或 Intersection Observer API,延迟加载视口外的图片。 - 格式优化 :优先使用 WebP 或 AVIF 格式,相比 JPEG/PNG 可减少 25%-50% 体积。
- 图标字体/ SVG Sprite:合并小图标,减少 HTTP 请求数。
2. 代码分割 (Code Splitting)
-
路由级懒加载 :
javascript// React + React Router const Home = React.lazy(() => import('./Home')); <Suspense fallback={<Loading />}> <Home /> </Suspense>
-
组件级分割:将非首屏组件拆分。
-
第三方库分割 :将
lodash
,moment
等大库单独打包,利用长期缓存。
3. 资源预加载与预解析
html
<!-- 预加载关键资源(高优先级) -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.webp" as="image">
<!-- 预解析 DNS -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- 预取未来可能用到的资源(低优先级) -->
<link rel="prefetch" href="/next-page.js">
4. 脚本加载策略
属性 | 行为 | 适用场景 |
---|---|---|
async |
下载不阻塞解析,下载完立即执行,执行时阻塞解析。 | 独立脚本,如统计代码。 |
defer |
下载不阻塞解析,DOM 解析完成后 ,DOMContentLoaded 前执行。 |
依赖 DOM 的脚本,如主应用逻辑。 |
默认 | 阻塞 HTML 解析,直到脚本下载并执行完。 | 极少数关键内联脚本。 |
📌 面试环节:资源加载
Q1: async
和 defer
的区别是什么?
A :
async
脚本下载完成后会立即执行 ,执行时会阻塞 HTML 解析,且执行顺序不确定。defer
脚本下载不阻塞解析,会在 DOM 解析完成后 ,DOMContentLoaded
事件触发前按顺序执行 。defer
适合需要操作 DOM 的脚本。
Q2: 如何优化图片加载?
A:
- 使用 WebP/AVIF 格式。
- 实施懒加载。
- 对于小图标,使用图标字体 或 SVG Sprite。
- 使用
srcset
和sizes
实现响应式图片。- 合理设置图片尺寸,避免过大。
Q3: 什么是代码分割(Code Splitting)?它有什么好处?
A: 代码分割是将应用代码拆分成多个小块的技术。好处包括:
- 减少首屏加载时间:只加载当前页面需要的代码。
- 利用浏览器缓存:公共库(如 React)可以长期缓存,业务代码更新时无需重新下载。
- 并行加载:多个小文件可以并行下载,提升速度。
三、JavaScript 执行优化:解放主线程
JS 执行在主线程,会阻塞渲染。
1. 防抖 (Debounce) 与 节流 (Throttle)
- 防抖 :事件触发后,等待
n
秒无新事件,才执行一次。适用于搜索框输入。 - 节流 :事件触发后,在
n
秒内最多执行一次。适用于scroll
、resize
事件。
2. Web Workers
将复杂计算(如大数据处理、图像编码)移出主线程,在后台线程执行,避免阻塞 UI。
javascript
const worker = new Worker('heavy-calc.js');
worker.postMessage(data);
worker.onmessage = (e) => { /* 处理结果 */ };
3. requestAnimationFrame
(rAF)
用于动画,确保在浏览器下一次重绘前执行,实现 60fps 流畅动画。
javascript
function animate() {
// 更新动画状态
requestAnimationFrame(animate); // 下一帧继续
}
requestAnimationFrame(animate);
4. requestIdleCallback
在浏览器空闲时期执行低优先级任务(如预加载、日志上报),避免影响关键渲染。
javascript
requestIdleCallback(() => {
// 执行非关键任务
});
📌 面试环节:JS 执行
Q1: 请手写一个防抖函数和一个节流函数。
A:
javascript// 防抖 function debounce(func, delay) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => func.apply(this, args), delay); }; } // 节流 function throttle(func, delay) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime > delay) { func.apply(this, args); lastTime = now; } }; }
Q2: Web Workers
是什么?它解决了什么问题?
A : Web Workers 允许在后台线程中运行 JavaScript 代码。它解决了长时间运行的 JS 任务阻塞主线程的问题,从而避免了 UI 冻结,提升了应用的响应性和流畅度。注意:Worker 线程不能直接操作 DOM。
Q3: requestAnimationFrame
有什么优势?
A :
rAF
由浏览器统一调度,能确保回调函数在下一次重绘前执行,从而与屏幕刷新率(通常 60Hz)同步,避免画面撕裂和卡顿,实现流畅动画。它还具备节流特性,并在标签页不可见时自动暂停,节省资源。
四、缓存策略:减少重复请求
1. HTTP 缓存
机制 | 请求 | 响应头 | 验证方式 |
---|---|---|---|
强缓存 | 无请求 | Cache-Control: max-age=3600 |
直接使用本地缓存。 |
协商缓存 | 发起请求 | ETag / Last-Modified |
服务端比对 If-None-Match / If-Modified-Since ,返回 304 Not Modified 。 |
2. 浏览器存储
localStorage
:持久化存储,容量大(~5-10MB),适合存储用户偏好、离线数据。sessionStorage
:会话级存储,关闭标签页后清除。Cookie
:自动随请求发送,用于身份认证(HttpOnly
安全),但体积小(~4KB)。
📌 面试环节:缓存
Q1: 强缓存和协商缓存的区别?
A : 强缓存(
Cache-Control
,Expires
)在有效期内直接使用本地缓存,不向服务器发请求 。协商缓存(ETag
/If-None-Match
,Last-Modified
/If-Modified-Since
)会向服务器发起请求,服务器根据资源是否变化决定返回304
(使用缓存)还是200
(返回新资源)。
Q2: ETag
和 Last-Modified
有什么优劣?
A :
Last-Modified
基于时间戳,精度为秒,如果文件在 1 秒内被修改多次,可能检测不到变化。ETag
是文件内容的哈希值(如 MD5),能精确检测任何内容变化。ETag
更精确,但生成开销稍大。
Q3: localStorage
和 Cookie
的主要区别?
A:
- 大小 :
localStorage
(~5-10MB) 远大于Cookie
(~4KB)。- 发送 :
localStorage
不会自动随 HTTP 请求发送;Cookie
会自动发送,用于身份认证。- 作用域 :
localStorage
严格按同源策略;Cookie
可通过Domain
和Path
设置作用域。- 安全性 :
Cookie
可设置HttpOnly
(防 XSS)和Secure
(仅 HTTPS)。
五、网络与传输优化
1. CDN (Content Delivery Network)
- 将静态资源(JS, CSS, 图片, 字体)分发到全球边缘节点。
- 用户就近访问,降低延迟,减轻源服务器压力。
2. 压缩
- Gzip/Brotli:对文本资源(HTML, CSS, JS)进行压缩,通常可减少 60%-80% 体积。
3. HTTP/2
- 多路复用:单个连接上并行传输多个请求/响应,解决 HTTP/1.1 的队头阻塞。
- 头部压缩:减少请求头体积。
- 服务器推送:服务器可主动推送客户端可能需要的资源(但需谨慎使用)。
4. DNS 预解析
提前解析未来可能用到的域名的 IP 地址,减少 DNS 查询延迟。
📌 面试环节:网络
Q1: CDN 的工作原理是什么?
A: CDN 通过在全球部署边缘节点服务器,将源站的静态资源缓存到离用户最近的节点。用户请求时,DNS 解析到最近的 CDN 节点,节点直接返回缓存资源,大幅降低延迟和源站压力。
Q2: HTTP/2 相比 HTTP/1.1 有哪些改进?
A: 主要改进:
- 二进制分帧:数据以二进制帧传输,更高效。
- 多路复用:一个连接上可并行处理多个请求/响应,彻底解决队头阻塞。
- 头部压缩:使用 HPACK 算法压缩头部,减少开销。
- 服务器推送:服务器可主动推送资源。
六、首屏性能优化:提升用户体验
1. 服务端渲染 (SSR)
- 原理:在服务器端生成完整的 HTML 字符串,直接发送给浏览器。
- 优势:首屏内容立即可见(FCP/LCP 极佳),SEO 友好。
- 框架:Next.js (React), Nuxt.js (Vue)。
2. 骨架屏 (Skeleton Screen)
- 在数据加载期间,显示一个"骨架"占位符(灰色块),让用户感知到内容正在加载,减少"白屏"焦虑。
3. 关键渲染路径优化
- 最小化关键 CSS/JS:只加载首屏必需的代码。
- 内联关键 CSS:避免关键 CSS 的网络请求。
📌 面试环节:首屏优化
Q1: SSR 和 CSR(客户端渲染)的区别?SSR 的优势是什么?
A : CSR 在浏览器下载 JS 后才开始渲染页面,首屏时间长,SEO 差。SSR 在服务器端就完成了 HTML 渲染,浏览器直接显示内容,首屏速度快,利于 SEO。但 SSR 会增加服务器压力。
Q2: 什么是核心 Web 指标(Core Web Vitals)?
A: Google 定义的关键用户体验指标:
- LCP (Largest Contentful Paint):最大内容绘制时间(< 2.5s 为好)。
- FID (First Input Delay):首次输入延迟(< 100ms 为好)。
- CLS (Cumulative Layout Shift):累积布局偏移(< 0.1 为好)。
七、性能监控与测试
1. Chrome DevTools
- Performance 面板:录制页面加载过程,分析 FPS、CPU、内存、网络、重排重绘等。
- Lighthouse :自动化审计工具,对性能、可访问性、最佳实践、SEO、PWA 进行评分,并提供详细优化建议(如图片优化、消除渲染屏蔽资源)。
📌 面试环节:监控
Q1: 如何监控前端性能?
A:
- 开发阶段:Chrome DevTools Performance 面板、Lighthouse。
- 生产阶段:集成 RUM(Real User Monitoring)工具,如 Sentry、Datadog、阿里云 ARMS,监控真实用户的 FCP、LCP、FID、CLS 等指标。
八、总结:构建高性能应用的思维
前端性能优化是一个系统工程 ,需要从渲染、资源、执行、缓存、网络、体验等多个维度协同考虑。没有银弹,只有持续的监控、分析和迭代。核心原则是:
- 减少:减少资源体积、请求数量、关键路径长度。
- 延迟:延迟非关键资源的加载和执行。
- 缓存:充分利用各级缓存。
- 并行:利用多路复用、并发加载。
- 测量:用数据驱动优化,关注真实用户的核心体验指标。