性能优化保姆级指南,一篇搞定所有考点💯

大家好!在前端面试中,"性能优化" 绝对是高频考点 ------ 毕竟用户体验直接影响产品生死,而性能就是体验的核心。这篇文章会从浏览器底层原理 出发,层层递进讲到框架层优化 和工程化实践,每个知识点都拆解得足够细致

浏览器渲染机制与性能瓶颈

要做性能优化,首先得知道 "性能瓶颈" 在哪。浏览器渲染页面的核心流程叫关键渲染路径(Critical Rendering Path) ,步骤如下:

  1. 解析 HTML → 生成 DOM 树(处理标签和文本,忽略 CSS/JS)
  2. 解析 CSS → 生成 CSSOM 树(处理样式,不管 HTML 结构)
  3. DOM + CSSOM → 生成渲染树 (只包含 "需要显示的节点",比如隐藏的display: none元素不会进入渲染树)
  4. 布局(Layout) :计算渲染树中每个节点的尺寸、位置(这一步就是 "重排" 的核心)
  5. 绘制(Paint) :把节点的样式(颜色、背景等)画到屏幕上(这一步就是 "重绘" 的核心)
  6. 合成(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:

  1. 打开 F12 → 切换到 Performance 面板
  2. 点击 "录制" 按钮,操作页面
  3. 录制结束后,查看 "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 种实现方式,按推荐度排序:

  1. 原生懒加载 (最简单,推荐):给<img>加loading="lazy"属性(兼容性:Chrome 77+,Edge 79+)

    html 复制代码
    <img src="image.jpg" alt="..." loading="lazy" width="400" height="300">
  2. IntersectionObserver API (现代方案,性能好):监听图片是否进入视口,进入后再设置src

    js 复制代码
    const 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);
    });
  3. 传统 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 可以创建 "子线程",在子线程中执行复杂计算,不影响主线程。

用法步骤:

  1. 创建 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);
    }
  2. 主线程中使用 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 缓存的完整流程?

  1. 浏览器请求资源,先查强缓存(Cache-Control/Expires):

    • 未过期:直接用本地缓存(200 OK (from memory cache/disk cache))
    • 已过期:进入协商缓存
  2. 发送请求到服务器,携带协商缓存头(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 下传输)属性:

http 复制代码
Set-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",首屏加载更快。

原理:

  1. 用户请求页面 → 服务器接收请求。
  2. 服务器执行 JS(比如 React/Vue 组件),渲染出完整的 HTML。
  3. 服务器把 HTML 返回给浏览器 → 浏览器直接显示页面(无需等待 JS 加载执行)。
  4. 浏览器加载 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 });

总结

前端性能优化是一个 "从底层到上层" 的系统工程,核心逻辑可以总结为三点:

  1. 减少资源体积:压缩图片、JS、CSS,选择合适的格式(如 WebP),拆分代码(Code Splitting)。
  2. 加快资源加载:用 CDN 加速、HTTP 缓存、预加载,优化网络协议(HTTP/2/3)。
  3. 减少渲染阻塞:避免重排重绘,用 GPU 加速(transform),优化 JS 执行(防抖、节流、Web Workers)。

掌握这些技巧不仅能应对面试,更能在实际项目中提升用户体验 ------ 毕竟,"快" 就是最好的体验之一。

相关推荐
江华森9 分钟前
Web漏洞与中间件框架漏洞实战详解
面试
lovingsoft12 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly22 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝26 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦27 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡1 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
蒸鱼Yuzheng1 小时前
Shader 指令成本回归:离线编译、设备权重与误报控制
ci/cd·性能优化·gpu·shader·vulkan
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring