秋招面试---性能优化(良子大胃袋)

在秋招面试中经常会遇到面试官问:"你的项目中有哪些性能优化、你知道的性能优化有哪些?" 本篇文章小编将给大家讲讲性能优化这个话题(如有没涉及到的欢迎在评论区补充)


一、渲染性能基石:理解重排(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: 主要策略有:

  1. 批量操作 :使用 cssText 或切换 className 一次性修改样式。
  2. 文档片段 :使用 DocumentFragment 批量添加节点。
  3. 脱离文档流 :操作前将元素 display: noneposition: absolute,操作完再恢复。
  4. 缓存布局信息 :避免在循环中读取 offsetTop 等布局属性。
  5. 使用 transform :用 transform 代替 left/top 等位置属性进行动画。

Q3: 为什么 transformleft/top 性能更好?

A : left/top 会修改元素的几何属性,触发重排和重绘。而 transform 属于合成层操作,通常由 GPU 处理,不触发主线程的重排和重绘,只可能触发合成(Compositing),性能开销小得多。


二、资源加载优化:减少等待时间

资源加载是影响首屏时间(FCP, LCP)的关键。

1. 图片优化

  • 懒加载 (Lazy Loading)loading="lazy" 或 Intersection Observer API,延迟加载视口外的图片。
  • 格式优化 :优先使用 WebPAVIF 格式,相比 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: asyncdefer 的区别是什么?

A : async 脚本下载完成后会立即执行 ,执行时会阻塞 HTML 解析,且执行顺序不确定。defer 脚本下载不阻塞解析,会在 DOM 解析完成后DOMContentLoaded 事件触发前按顺序执行defer 适合需要操作 DOM 的脚本。

Q2: 如何优化图片加载?

A:

  1. 使用 WebP/AVIF 格式。
  2. 实施懒加载
  3. 对于小图标,使用图标字体SVG Sprite
  4. 使用 srcsetsizes 实现响应式图片
  5. 合理设置图片尺寸,避免过大。

Q3: 什么是代码分割(Code Splitting)?它有什么好处?

A: 代码分割是将应用代码拆分成多个小块的技术。好处包括:

  1. 减少首屏加载时间:只加载当前页面需要的代码。
  2. 利用浏览器缓存:公共库(如 React)可以长期缓存,业务代码更新时无需重新下载。
  3. 并行加载:多个小文件可以并行下载,提升速度。

三、JavaScript 执行优化:解放主线程

JS 执行在主线程,会阻塞渲染。

1. 防抖 (Debounce) 与 节流 (Throttle)

  • 防抖 :事件触发后,等待 n 秒无新事件,才执行一次。适用于搜索框输入。
  • 节流 :事件触发后,在 n 秒内最多执行一次。适用于 scrollresize 事件。

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: ETagLast-Modified 有什么优劣?

A : Last-Modified 基于时间戳,精度为秒,如果文件在 1 秒内被修改多次,可能检测不到变化。ETag 是文件内容的哈希值(如 MD5),能精确检测任何内容变化。ETag 更精确,但生成开销稍大。

Q3: localStorageCookie 的主要区别?

A:

  • 大小localStorage (~5-10MB) 远大于 Cookie (~4KB)。
  • 发送localStorage 不会自动随 HTTP 请求发送;Cookie 会自动发送,用于身份认证。
  • 作用域localStorage 严格按同源策略;Cookie 可通过 DomainPath 设置作用域。
  • 安全性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: 主要改进:

  1. 二进制分帧:数据以二进制帧传输,更高效。
  2. 多路复用:一个连接上可并行处理多个请求/响应,彻底解决队头阻塞。
  3. 头部压缩:使用 HPACK 算法压缩头部,减少开销。
  4. 服务器推送:服务器可主动推送资源。

六、首屏性能优化:提升用户体验

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:

  1. 开发阶段:Chrome DevTools Performance 面板、Lighthouse。
  2. 生产阶段:集成 RUM(Real User Monitoring)工具,如 Sentry、Datadog、阿里云 ARMS,监控真实用户的 FCP、LCP、FID、CLS 等指标。

八、总结:构建高性能应用的思维

前端性能优化是一个系统工程 ,需要从渲染、资源、执行、缓存、网络、体验等多个维度协同考虑。没有银弹,只有持续的监控、分析和迭代。核心原则是:

  1. 减少:减少资源体积、请求数量、关键路径长度。
  2. 延迟:延迟非关键资源的加载和执行。
  3. 缓存:充分利用各级缓存。
  4. 并行:利用多路复用、并发加载。
  5. 测量:用数据驱动优化,关注真实用户的核心体验指标。
相关推荐
PAK向日葵30 分钟前
【算法导论】XM 0823 笔试题解
算法·面试
Liu.7742 小时前
uniappx鸿蒙适配
前端
山有木兮木有枝_3 小时前
从代码到创作:探索AI图片生成的神奇世界
前端·coze
ZXT3 小时前
js基础重点复习
javascript
WebInfra4 小时前
Rspack 1.5 发布:十大新特性速览
前端·javascript·github
雾恋5 小时前
我用 trae 写了一个菜谱小程序(灶搭子)
前端·javascript·uni-app
烛阴5 小时前
TypeScript 中的 `&` 运算符:从入门、踩坑到最佳实践
前端·javascript·typescript
Java 码农6 小时前
nodejs koa留言板案例开发
前端·javascript·npm·node.js
ZhuAiQuan6 小时前
[electron]开发环境驱动识别失败
前端·javascript·electron