【performance面试考点】让面试官眼前一亮的performance性能优化

1,213 阅读23分钟

【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 核心流程:
  1. 用户请求页面 → 服务器接收请求;
  1. 服务器执行 JS(如 Next.js/Nuxt.js),获取数据、渲染组件,生成完整 HTML;
  1. 服务器将 HTML 发送给浏览器;
  1. 浏览器直接渲染 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-ModifiedIf-Modified-Since基于资源修改时间戳优点:计算简单;缺点:精度到秒,文件内容不变但时间变会误判
ETagIf-None-Match基于资源内容哈希值优点:精度高;缺点:计算哈希耗服务器性能
HTTP 缓存流程总结:
  1. 浏览器请求资源 → 检查强缓存(Cache-Control/Expires);
  1. 强缓存未过期 → 直接使用本地缓存;
  1. 强缓存过期 → 发送请求到服务器,携带协商缓存字段(If-Modified-Since/If-None-Match);
  1. 服务器判断资源未变化 → 返回 304,浏览器使用本地缓存;
  1. 服务器判断资源已变化 → 返回 200 和新资源,更新本地缓存。

2. 客户端缓存:localStorage/sessionStorage/cookie

用于缓存客户端数据(如用户信息、配置项),减少接口请求。

特性localStoragesessionStoragecookie
存储大小5MB5MB4KB
有效期永久(除非手动删除)会话级(标签页关闭消失)可设置过期时间(如 1 天)
作用域同域名下所有标签页仅当前标签页同域名(可设置 path/domain)
通信方式仅客户端,不随请求发送仅客户端,不随请求发送随每次 HTTP 请求发送(可设置 httpOnly)
安全性低(易被 XSS 攻击)低(易被 XSS 攻击)可设置 httpOnly(防 XSS)、Secure(仅 HTTPS)、SameSite(防 CSRF)
适用场景:
  • localStorage:存储用户偏好(如主题、语言)、长期有效的数据;
  • sessionStorage:存储临时表单数据、会话级数据(如验证码);
  • cookie:存储身份认证信息(如 token,需设置httpOnly)、跨域请求携带的小数据。

七、性能测试:用工具量化优化效果

优化不能凭感觉,需要用工具量化性能指标,找到瓶颈,验证优化效果。

1. Chrome DevTools Performance:分析渲染和执行瓶颈

使用步骤:
image.png
  1. 打开 Chrome → F12 打开 DevTools → 切换到 Performance 标签;
  1. 点击「Record」按钮(圆形按钮)→ 操作页面(如刷新、点击)→ 点击「Stop」;
  1. 分析报告:
    • 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、最佳实践等,生成详细优化建议。 image.png

使用步骤:
  1. Chrome DevTools → Lighthouse 标签 → 勾选「Performance」等选项;
  1. 点击「Generate report」→ 等待测试完成;
  1. 查看报告:
    • 性能分数(0~100,80 + 为优秀);
    • 具体优化建议(如 “压缩图片”“启用 Gzip”“减少 JS 体积”)。
image.png

八、面试总结:如何让面试官眼前一亮?

讲性能优化时,别只说 “要做图片懒加载、防抖节流”,这样太简单了。我们想要让面试官眼前一亮,最主要的向让面试官展现出我们基础知识的牢固、全面和你有实践的经历

所有核心是体现  “理解底层 + 注重实践” —— 既要讲清方案原理,也要说清落地细节和量化效果

AI生成参考回答

  1. 基础核心(重绘重排) :先讲浏览器渲染流水线,再对比重绘重排定义与区别,接着给出 6 个实战优化方>案(合并样式操作、文档碎片、脱离文档流、缓存布局信息、transform 替代传统样式、避免强制同步布局),每个>方案配代码示例与底层逻辑。
  2. 资源加载优化:聚焦图片(WebP、懒加载、压缩裁剪)、脚本(async/defer/module 对比)、资源预加载(preload/prefetch/dns-prefetch)等,讲清每种优化的底层逻辑(如 WebP 压缩原理、脚本加载阻塞机制)与实践方式。
  3. JS 执行优化:围绕主线程不阻塞,讲解防抖节流(手写核心代码 + 适用场景)、Web Workers(子线程原理 + 实战案例)、requestAnimationFrame/requestIdleCallback(动画与空闲任务优化逻辑)。
  4. 框架层优化:分 React(memo/useMemo/useCallback)与 Vue(v-memo/computed/keep-alive),结合框架特性讲优化逻辑,补充 key 使用、组件按需加载等通用技巧。
  5. 首屏优化(重点) :针对大厂关注点,讲解 SSR(流程、优缺、框架)、骨架屏(实现方式)、代码分割等方案,强调首屏优化对用户留存的影响。
  6. 缓存策略:对比 HTTP 缓存(强缓存 vs 协商缓存核心字段与流程)与客户端缓存(localStorage/sessionStorage/cookie 特性与场景),讲清缓存复用逻辑。
  7. 性能测试工具:介绍 Chrome Performance(分析步骤、核心指标)与 Lighthouse(使用步骤、报告解读),说明工具如何量化优化效果、定位瓶颈。

将性能优化讲的全面、细致。透露出你有实践经历,可以直接上手工作。这样就能让你在面试官眼前一亮。

最后:性能优化的核心思路

性能优化不是 “一蹴而就” 的,而是 “持续迭代” 的过程,核心思路可以总结为 3 点:

  1. 减少资源体积:压缩 JS/CSS、用 WebP 图片、代码分割;
  1. 减少请求次数:合并请求、缓存资源、懒加载;
  1. 优化执行效率:避免重排重绘、减少主线程阻塞、利用 GPU 加速。

记住:性能优化没有 “银弹”,需要结合项目场景(如首屏、列表、动画)选择合适的方案,并用工具量化效果,才能真正提升用户体验。