前端高频面试题总结_性能_工程化_网络

前端高频面试题


目录

  • 第一部分:前端性能优化
    • [Q1: 谈谈你对 Core Web Vitals (核心网页指标) 的理解及优化手段](#Q1: 谈谈你对 Core Web Vitals (核心网页指标) 的理解及优化手段)
    • [Q2: 浏览器渲染流程是怎样的?如何避免重排(Reflow)与重绘(Repaint)?](#Q2: 浏览器渲染流程是怎样的?如何避免重排(Reflow)与重绘(Repaint)?)
    • [Q3: 虚拟列表(Virtual List)的实现原理是什么?](#Q3: 虚拟列表(Virtual List)的实现原理是什么?)
    • [Q4: 如何避免长任务阻塞主线程?](#Q4: 如何避免长任务阻塞主线程?)
  • 第二部分:前端工程化
    • [Q1: 深度对比 Webpack 与 Vite 的构建机制与差异](#Q1: 深度对比 Webpack 与 Vite 的构建机制与差异)
    • [Q2: Tree Shaking 的底层原理是什么?CommonJS 和 ESM 有何不同?](#Q2: Tree Shaking 的底层原理是什么?CommonJS 和 ESM 有何不同?)
    • [Q3: 微前端(如 qiankun)的沙箱隔离机制是如何实现的?](#Q3: 微前端(如 qiankun)的沙箱隔离机制是如何实现的?)
    • [Q4: Webpack HMR(热更新)的工作流与核心原理](#Q4: Webpack HMR(热更新)的工作流与核心原理)
  • 第三部分:网络与传输
    • [Q1: HTTP/1.1、HTTP/2、HTTP/3 的演进及性能改进](#Q1: HTTP/1.1、HTTP/2、HTTP/3 的演进及性能改进)
    • [Q2: 强缓存与协商缓存的完整决策流程及常见应用场景](#Q2: 强缓存与协商缓存的完整决策流程及常见应用场景)
    • [Q3: HTTPS 的加密握手及秘钥协商完整过程](#Q3: HTTPS 的加密握手及秘钥协商完整过程)
    • [Q4: WebRTC 的 P2P 连接建立过程(信令与打洞)](#Q4: WebRTC 的 P2P 连接建立过程(信令与打洞))

第一部分:前端性能优化

Q1: 谈谈你对 Core Web Vitals (核心网页指标) 的理解及优化手段

1. 核心指标解析
  • LCP (Largest Contentful Paint - 最大内容绘制) :测量加载性能 。要求页面首次开始加载后的 2.5秒 内,视口内最大的图片或文本块渲染完成。
  • FID (First Input Delay - 首次输入延迟) / INP (Interaction to Next Paint - 交互到下次绘制) :测量交互响应性
    • 注:INP 已在 2024 年 3 月正式取代 FID。INP 衡量用户在访问期间做出的所有交互的完整延迟,优秀标准为小于 200毫秒
  • CLS (Cumulative Layout Shift - 累计布局偏移) :测量视觉稳定性 。衡量可见元素在视觉上发生意外偏移的程度,优秀标准为小于 0.1
2. 指标监控思路(PerformanceObserver)
  • 第一步 :创建 PerformanceObserver 监听实例。
  • 第二步 :注册感兴趣 of 性能条目类型(如 largest-contentful-paint, layout-shift, first-input)。
  • 第三步 :在回调中收集指标数据并进行上报(利用 navigator.sendBeacon 进行无阻塞上报)。
【结构化伪代码:CLS 与 LCP 监控上报逻辑】
javascript 复制代码
// 1. 初始化指标累加器
LCP数值 = 0
CLS数值 = 0

// 2. 创建性能观测器
创建 性能观测器 (条目列表) {
  对于 列表中的每个 条目 {
    如果 (条目类型 是 "largest-contentful-paint") {
      // 持续更新最大值,因为 LCP 会随着加载过程不断更新
      LCP数值 = 条目.开始时间
    }
    如果 (条目类型 是 "layout-shift" 并且 条目没有用户近期输入动作) {
      CLS数值 = CLS数值 + 条目.偏移值
    }
  }
}

// 3. 开启监听
性能观测器.监听({ 
  条目类型集合: ["largest-contentful-paint", "layout-shift"], 
  是否缓存已有数据: 真 
})

// 4. 页面卸载时上报
当 页面即将关闭或隐藏时 {
  数据包 = 格式化为JSON({ lcp: LCP数值, cls: CLS数值 })
  浏览器异步免阻碍发送(上报接口URL, 数据包)
}
【CLS 与 LCP 监控上报】
javascript 复制代码
let lcpValue = 0;
let clsValue = 0;

try {
  const observer = new PerformanceObserver((entryList) => {
    for (const entry of entryList.getEntries()) {
      // 收集最大内容绘制时间 (LCP)
      if (entry.entryType === 'largest-contentful-paint') {
        lcpValue = entry.startTime;
      }
      // 收集非用户主动交互引起的意外布局偏移量 (CLS)
      if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) {
        clsValue += entry.value;
      }
    }
  });

  // 开始监测对应的性能条目,并缓存旧指标数据
  observer.observe({ entryTypes: ['largest-contentful-paint', 'layout-shift'], buffered: true });
} catch (e) {
  console.warn('当前浏览器不支持 PerformanceObserver 性能监控 API。', e);
}

// 页面即将销毁/隐藏时,利用无阻塞的 sendBeacon 异步上报指标数据
window.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    const payload = JSON.stringify({ lcp: lcpValue, cls: clsValue });
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/report/performance', payload);
    } else {
      // fallback:传统异步请求,设置 keepalive 保证页面关闭时也能发出
      fetch('/api/report/performance', { method: 'POST', body: payload, keepalive: true });
    }
  }
});
3. 核心优化手段
  • 优化 LCP
    • 预加载关键资源 :对 LCP 元素图片使用 <link rel="preload" as="image">
    • 移除阻塞渲染的 JS/CSS :使用 async / defer,内联关键 CSS。
    • 服务端渲染 (SSR) / 静态生成 (SSG):缩短获取 HTML 的网络耗时。
  • 优化 INP/FID
    • 拆分长任务 (Long Tasks):将占用超过 50ms 的 JavaScript 任务切片执行。
    • 减少主线程阻塞:将非 UI 相关的重计算逻辑移入 Web Worker。
    • 优化防抖与节流:避免高频交互触发密集重渲染。
  • 优化 CLS
    • 为图片和媒体保留宽高属性 :显式书写 widthheight,或使用 CSS 的 aspect-ratio
    • 避免动态插入无尺寸内容:广告位、动态 banner 等设置最小骨架高度。
    • CSS 动画优化 :优先使用 transform / opacity 进行位移动画,避免使用 top / left / height

Q2: 浏览器渲染流程是怎样的?如何避免重排(Reflow)与重绘(Repaint)?

1. 浏览器经典渲染流水线
  1. 构建 DOM 树:解析 HTML,生成 DOM 树。
  2. 构建 CSSOM 树:解析 CSS(包含外部和内联样式),生成样式结构树。
  3. 合并为 Render Tree(渲染树) :将 DOM 树与 CSSOM 树合并,剔除不可见节点(如 display: none)。
  4. Layout(重排/回流):计算渲染树中每个节点在屏幕上的几何大小与绝对坐标。
  5. Paint(重绘):绘制节点的背景、颜色、边框、文本等视觉信息。
  6. Composite(图层合成):将各个渲染图层(Layers)合并,发送至 GPU 进行渲染输出。
2. 重排与重绘的根本区别
  • 重排(Reflow) :当元素的几何属性(如宽、高、外边距、定位、显示状态等)发生改变,浏览器必须重新计算元素的位置 and 大小。重排必定导致重绘。
  • 重绘(Repaint) :当元素的视觉外观属性(如背景色、文字颜色、阴影等)改变,但几何尺寸未变,浏览器仅需重新绘制该元素。重绘不一定会触发重排。
3. 避免重排与重绘的实战策略
  • 合并样式修改 :避免逐条修改样式,使用合并 class 或 cssText
  • 读写分离(防范强制同步布局)
    • 问题根源 :当在代码中修改了样式,紧接着读取 offsetWidthscrollTop 等几何属性时,浏览器为了提供准确的读取值,会被迫立即触发一次重排。
    • 解决方案 :使用 FastDOM 思想,将读取操作和写入操作归类合并。
  • 离线操作 DOM
    • 使用 DocumentFragment 临时缓存 DOM 修改,一次性挂载。
    • 修改频繁的元素可以先设为 display: none(触发一次重排),修改完后再显式化(再触发一次重排)。
  • 利用 CSS 硬件加速 (GPU 加速)
    • 通过 transformopacitywill-change 属性将元素提升为独立合成层 (Compositing Layer)
    • 提升为独立合成层后,该元素的变动只会触发 Composite(图层合成)阶段,完全绕过 LayoutPaint 阶段。

Q3: 虚拟列表(Virtual List)的实现原理是什么?

1. 核心思路分析

虚拟列表是一种针对**超长数据列表(如 10 万条数据)**的极致渲染优化手段。其核心原理是:只渲染用户当前视口可见的元素,通过绝对定位和填充容器高度来模拟真实滚动

  • 容器高度(Viewport Container) :固定高度,设置 overflow-y: auto
  • 占位滚动区域(Scroll Phantom) :绝对定位,高度为 单项高度 * 总数据项数,用于支撑起滚动条。
  • 真实渲染列表(Render List) :绝对定位在容器顶部,只渲染 视口高度 / 单项高度 + 缓冲区个数 项。
  • 偏移量(Offset) :当滚动发生时,计算当前滚动了多少项,动态改变真实列表的 translateY,使其始终停留在视口内。
【结构化伪代码:虚拟列表滚动计算核心逻辑】
javascript 复制代码
// 1. 初始化常量与变量
视口高度 = 容器.当前可见高度
单项高度 = 50
总数据列表 = [...]
总项数 = 总数据列表.长度
缓冲区大小 = 3 // 避免滚动露白

// 页面可见渲染项数
可见项数 = 向上取整(视口高度 / 单项高度)
// 虚拟列表容器的总仿真高度
占位高度 = 总项数 * 单项高度

// 2. 监听滚动事件的核心回调
当容器触发滚动(滚动事件) {
  当前滚动高度 = 滚动事件.目标.scrollTop

  // 计算开始渲染的索引
  开始索引 = 向下取整(当前滚动高度 / 单项高度)
  // 加入缓冲区防露白
  安全开始索引 = 取最大值(0, 开始索引 - 缓冲区大小)

  // 计算结束渲染的索引
  结束索引 = 开始索引 + 可见项数
  安全结束索引 = 取最小值(总项数, 结束索引 + 缓冲区大小)

  // 提取出当前应当渲染的数据切片
  当前渲染数据 = 总数据列表.切片(安全开始索引, 安全结束索引)

  // 计算当前渲染区域的偏移位移,保持渲染节点处于视口内
  当前偏移量 = 安全开始索引 * 单项高度

  // 更新 DOM 状态
  更新视图({
    列表偏移样式: `translateY(${当前偏移量}px)`,
    渲染数据列表: 当前渲染数据
  })
}
【正确可执行的 JS 代码:虚拟列表滚动计算核心】
javascript 复制代码
// 假设 HTML 结构如下:
// <div id="list-container" style="height: 400px; overflow-y: auto; position: relative;">
//   <div id="list-phantom" style="position: absolute; left: 0; top: 0; right: 0; z-index: -1;"></div>
//   <div id="list-actual" style="position: absolute; left: 0; top: 0; right: 0;"></div>
// </div>

const container = document.getElementById('list-container');
const phantom = document.getElementById('list-phantom');
const list = document.getElementById('list-actual');

const itemHeight = 50;         // 单项高度
const totalCount = 100000;     // 总计 10 万条数据
const bufferSize = 3;          // 缓冲区项数

const viewportHeight = container.clientHeight;
const visibleCount = Math.ceil(viewportHeight / itemHeight); // 视口内最多容纳项数

// 1. 初始化仿真占位高度,撑开滚动条
phantom.style.height = `${totalCount * itemHeight}px`;

// 模拟海量原始数据
const allData = Array.from({ length: totalCount }, (_, i) => `列表项内容 #${i + 1}`);

// 2. 监听滚动事件
container.addEventListener('scroll', (e) => {
  const scrollTop = e.target.scrollTop;

  // 3. 计算可见区域的数据切片索引
  const startIndex = Math.floor(scrollTop / itemHeight);
  const safeStartIndex = Math.max(0, startIndex - bufferSize);

  const endIndex = startIndex + visibleCount;
  const safeEndIndex = Math.min(totalCount, endIndex + bufferSize);

  // 4. 计算渲染区域在父容器中的绝对偏移位移(以保持显示在视口内)
  const offset = safeStartIndex * itemHeight;
  list.style.transform = `translate3d(0, ${offset}px, 0)`;

  // 5. 更新 DOM 渲染
  updateDOM(allData.slice(safeStartIndex, safeEndIndex));
});

function updateDOM(renderData) {
  const fragment = document.createDocumentFragment();
  renderData.forEach((text) => {
    const el = document.createElement('div');
    el.className = 'list-item';
    el.style.height = `${itemHeight}px`;
    el.style.lineHeight = `${itemHeight}px`;
    el.style.boxSizing = 'border-box';
    el.style.borderBottom = '1px solid #eee';
    el.innerText = text;
    fragment.appendChild(el);
  });
  list.innerHTML = '';
  list.appendChild(fragment);
}

// 首次主动触发一次渲染初始化
container.dispatchEvent(new Event('scroll'));

Q4: 如何避免长任务(Long Tasks)阻塞主线程?

1. 什么是长任务?

浏览器的主线程负责解析 HTML、CSS,执行 JavaScript,以及处理用户交互。当一个 JavaScript 任务执行时间超过 50毫秒 时,就会被定义为长任务 (Long Task)。长任务会导致浏览器无法及时响应用户操作,造成掉帧或卡顿(即影响 INP 指标)。

2. 优化思路一:时间分片 (Time Slicing)
  • 原理:将一个庞大的任务拆分成若干个在 50ms 内完成的子任务。在子任务执行完的间隙,将控制权交还给浏览器,让其有机会处理绘制和用户事件,然后再继续执行下一个子任务。
  • 主要 API 选型
    • requestIdleCallback:在浏览器空闲时期依次执行低优先级任务。
    • MessageChannel / setTimeout:利用宏任务将任务排队到下一次事件循环。
    • scheduler.postTask:现代浏览器原生调度 API。
【结构化伪代码:大数组处理的时间分片调度器】
javascript 复制代码
// 任务队列
待处理任务队列 = [...]
每次处理批次大小 = 100

函数 时间分片调度() {
  如果 (待处理任务队列 为空) {
    返回 // 任务全部结束
  }

  // 记录开始执行的时间戳
  开始时间 = 获取当前时间戳()

  // 循环执行,直到超时(例如超过16ms,保证60fps渲染)
  循环 (队列不为空 并且 (获取当前时间戳() - 开始时间 < 16)) {
    // 弹出一批任务执行
    单次批次任务 = 待处理任务队列.取出(每次处理批次大小)
    执行业务处理(单次批次任务)
  }

  // 释放主线程:通过宏任务将剩余任务调度到下一个 tick 执行
  如果 (待处理任务队列 不为空) {
    注册宏任务(时间分片调度)
  }
}
【正确可执行的 JS 代码:时间分片调度器】
javascript 复制代码
/**
 * 时间分片处理器
 * @param {Array} tasks 待处理的大批次任务数组
 * @param {number} batchSize 每次循环处理的任务批次大小
 * @param {Function} onProcess 处理任务的回调函数
 */
function timeSlicing(tasks, batchSize = 100, onProcess) {
  if (!tasks || tasks.length === 0) return;

  const queue = [...tasks];

  function run() {
    if (queue.length === 0) return;

    const startTime = performance.now();

    // 限制单个 tick 连续执行耗时不超过 16ms(约 60fps 一帧的渲染间隔),防阻塞主线程
    while (queue.length > 0 && (performance.now() - startTime < 16)) {
      const batch = queue.splice(0, batchSize);
      onProcess(batch);
    }

    // 如果任务没处理完,利用宏任务推迟到下一个事件循环 tick,释放主线程控制权
    if (queue.length > 0) {
      if (typeof MessageChannel !== 'undefined') {
        const channel = new MessageChannel();
        channel.port1.onmessage = run;
        channel.port2.postMessage(null);
      } else {
        setTimeout(run, 0);
      }
    }
  }

  run();
}

// 示例用法:
// const bigList = Array.from({ length: 50000 }, (_, i) => i);
// timeSlicing(bigList, 200, (items) => {
//   items.forEach(item => {
//     // 模拟某种重度逻辑计算
//     Math.sqrt(item) * Math.sin(item);
//   });
// });
3. 优化思路二:Web Worker 多线程计算
  • 原理:将纯计算型、与 DOM 渲染无关的长任务(如大文件 MD5 校验、图像处理、音频 DSP)完全剥离,扔到子线程(Web Worker)中运行。
  • 通信开销优化 :主线程与 Worker 之间使用 postMessage 通信,默认会复制数据造成额外开销。对于超大数据,应当使用 Transferable Objects(可转移对象,如 ArrayBuffer),以转移所有权的方式实现零拷贝。

第二部分:前端工程化

Q1: 深度对比 Webpack 与 Vite 的构建机制与差异

维度 Webpack Vite
底层核心语言 Node.js (JavaScript) Go (esbuild) + Rust (rollup/oxc插件) + JS
开发环境核心原理 编译打包后生成 Bundle,将 Bundle 提供给 Dev Server。 基于浏览器原生 ESM,无需预先打包,按需加载,即用即编译。
生产环境打包 Webpack 自身(丰富的 Loader 与 Plugin)。 Rollup(更加注重 Tree Shaking 和产物纯净度)。
热更新 (HMR) 速度 速度随项目模块体量增大而等比例变慢(需重构建依赖图)。 速度为常数级 (O(1)),只让修改模块失活并重新请求。
冷启动速度 慢(必须分析所有模块依赖,全量构建完毕才启动 Server)。 极快(直接启动 Server,依赖预构建交给 esbuild)。
1. Vite 开发环境下的"快"是如何实现的?
  1. 依赖预构建(Dependency Pre-bundling)
    • 痛点 :许多第三方依赖(如 lodash-es)有成百上千个 ESM 模块,浏览器一次性请求会造成网络风暴;且有些包使用的是 CommonJS 格式,浏览器无法解析。
    • 解决 :Vite 在首次启动时,利用 Go 编写的 esbuild 将第三方依赖快速打包成单个 ESM 包并存入 .vite 缓存文件夹。esbuild 速度比传统 JS 构建器快 10-100 倍。
  2. 按需编译
    • 当浏览器解析到 import { Page } from './views/Page.js' 时,才会向 Vite Dev Server 发起 HTTP 请求。
    • Vite 接收到请求后,再对当前文件进行单文件编译(如解析 Vue/SFC、Sass 等),然后返回。页面上未渲染的组件完全不参与编译。
2. Vite 既然这么好,为什么很多大型企业项目仍在使用 Webpack?
  • 生态与兼容性:Webpack 积累了十余年庞大的 Loader 和 Plugin 生态,几乎可以处理任何复杂的、古老的、非标的构建定制需求。
  • 开发与生产一致性:Vite 在开发环境使用 esbuild/原生ESM,而在生产环境使用 Rollup。这导致存在"开发环境运行正常,部署到生产环境报错"的边缘风险。而 Webpack 开发与生产均采用同一套编译器和依赖图谱。

Q2: Tree Shaking 的底层原理是什么?CommonJS 和 ESM 有何不同?

1. Tree Shaking 的核心定义

Tree Shaking(摇树优化)指的是在构建打包时,只保留实际执行到的代码,消除无用(Dead Code)代码,以减小最终打包体积。

2. 为什么 Tree Shaking 必须依赖 ESM?
  • CommonJS 的动态特性
    • CommonJS 的引入和导出是动态的(运行时的)。
    • 可以在 if 条件语句里进行 require,或者动态拼接路径:require('./libs/' + name)
    • 这意味着编译器在静态分析阶段,根本无法得知哪些模块被真正使用了。
  • ESM 的静态特性
    • ESM 的 importexport静态的(编译时的)。
    • 它们只能出现在模块的顶层,模块路径必须是静态字符串,不可包含变量。
    • 构建工具可以在不运行代码的情况下,通过静态分析构建出一条明确的**AST(抽象语法树)**依赖链,精准识别无用代码。
3. Tree Shaking 的工作步骤与机制
  1. AST 静态分析 :分析每个模块的 export,以及哪些 export 被其他模块 import 了。
  2. 标记无用代码 :标记那些虽有 export 却从未被 import 的声明。
  3. 消除死代码:在代码压缩(Minification)阶段(如使用 Terser 或 esbuild),将这些未被引用的函数/变量彻底从最终产物中剔除。
4. 面试高频深水坑:副作用(Side Effects)对 Tree Shaking 的影响
  • 什么是副作用 :当一个模块被导入时,除了导出变量,它还执行了其他影响外部环境的操作(例如:修改了全局变量 window.foo = 'bar',自执行了 CSS 注入,或者修改了原型链)。
  • 编译器的保守策略 :如果编译器在分析时发现某段未使用的代码可能带有副作用,为了保证程序的运行安全,它绝不敢摇掉这部分代码。
  • 如何规避
    • package.json 中配置 "sideEffects": false,告知构建工具该项目所有模块皆无副作用,可放心摇树。
    • 或指定 "sideEffects": ["*.css", "*.scss"],仅声明 CSS 文件有副作用。

Q3: 微前端(如 qiankun)的沙箱隔离机制是如何实现的?

微前端为了防止多个独立的子应用之间发生全局变量污染CSS 样式冲突 ,必须设计严格的沙箱机制。以下重点解析 JS 沙箱 的三种主流实现方案。

1. 单实例沙箱:快照沙箱 (SnapshotSandbox)
  • 原理 :在子应用激活时 ,记录当前 window 的快照;在子应用卸载时 ,通过对比当前 window 与快照的差异,将 window 还原,并将差异保存。等下次该子应用再次激活时,再把差异恢复回去。
  • 缺点 :必须遍历 window 上的所有属性,性能较差,且不支持同时运行多个子应用。
【结构化伪代码:快照沙箱还原原理】
javascript 复制代码
类 快照沙箱 {
  构造函数() {
    window快照 = {}
    增量修改记录 = {}
  }

  激活沙箱() {
    // 1. 记录当前 window 的状态快照
    对于 window 中的每个 属性 {
      window快照[属性] = window[属性]
    }

    // 2. 恢复上一次在此子应用运行中产生的增量修改
    对于 增量修改记录 中的每个 属性 {
      window[属性] = 增量修改记录[属性]
    }
  }

  失活沙箱() {
    对于 window 中的每个 属性 {
      如果 (window[属性] 不等于 window快照[属性]) {
        // 1. 记录发生了改变的属性(增量)
        增量修改记录[属性] = window[属性]
        // 2. 还原 window 属性到快照状态
        window[属性] = window快照[属性]
      }
    }
  }
}
【正确可执行的 JS 代码:快照沙箱】
javascript 复制代码
class SnapshotSandbox {
  constructor() {
    this.windowSnapshot = {}; // 用于存储激活时的 window 状态快照
    this.modifyMap = {};      // 记录子应用对 window 产生的增量修改
  }

  active() {
    // 1. 记录当前真实 window 的属性快照
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        this.windowSnapshot[prop] = window[prop];
      }
    }

    // 2. 恢复该子应用在上一次运行期间遗留下来的增量属性修改
    for (const prop in this.modifyMap) {
      if (this.modifyMap.hasOwnProperty(prop)) {
        window[prop] = this.modifyMap[prop];
      }
    }
  }

  inactive() {
    // 对比当前的全局 window 与之前的快照差异
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        if (window[prop] !== this.windowSnapshot[prop]) {
          // 1. 保存发生改变的增量修改
          this.modifyMap[prop] = window[prop];
          // 2. 还原当前 window 的变量状态,避免污染全局
          window[prop] = this.windowSnapshot[prop];
        }
      }
    }
  }
}

// 示例测试:
// const sandbox = new SnapshotSandbox();
// window.appName = 'global';
// sandbox.active();
// window.appName = 'sub-app-1'; // 子应用修改全局变量
// console.log(window.appName); // 'sub-app-1'
// sandbox.inactive();
// console.log(window.appName); // 'global' (全局 window 成功被还原)
2. 单实例升级版:遗留沙箱 (LegacySandbox)
  • 原理 :基于 ES6 Proxy,只代理单实例 window。它内部维护了修改记录、新增记录。通过 Proxyset 拦截属性变化,当卸载时直接反向应用这些记录来还原 window
3. 多实例沙箱:代理沙箱 (ProxySandbox)
  • 原理 :这是目前最完美 的方案。为每个子应用创建一个全新的伪 window 对象 (fakeWindow) 。当子应用运行时,所有的读写操作都通过 Proxy 进行拦截:
    • 写操作 :全部写入 fakeWindow 中,绝不污染真实的 window
    • 读操作 :优先从 fakeWindow 中读取;若找不到,再去真实的 window 上读取。
  • 优势 :由于每个子应用拥有独立的 fakeWindow 代理对象,真实 window 自始至终是干净的。因此,支持在同一个页面上并发运行多个子应用。
【结构化伪代码:多实例 Proxy 沙箱核心拦截逻辑】
javascript 复制代码
类 代理沙箱 {
  构造函数() {
    // 创建一个干净的伪 window 容器
    伪Window = 创建空对象()

    // 创建 Proxy 实例代理真实 window
    沙箱代理 = 新建 Proxy(window, {

      设置属性(目标真实Window, 属性Key, 新值) {
        // 所有写操作被拦截,只写入自己的伪 window
        伪Window[属性Key] = 新值
        返回 成功标志
      },

      获取属性(目标真实Window, 属性Key) {
        // 优先读取子应用内部自定义的属性
        如果 (属性Key 存在于 伪Window 中) {
          返回 伪Window[属性Key]
        }

        // 兜底读取全局 window 上的通用属性(如 setTimeout, document等)
        值 = 目标真实Window[属性Key]

        // 特殊处理:如果是系统方法,需要绑定 window 的 this 指向
        如果 (值 是 函数) {
          返回 值.绑定上下文(目标真实Window)
        }
        返回 值
      }
    })
  }
}
【多实例 Proxy 沙箱】
javascript 复制代码
class ProxySandbox {
  constructor() {
    const fakeWindow = Object.create(null); // 创建独立的伪 window 沙箱容器

    this.proxy = new Proxy(window, {
      set(target, prop, value) {
        // 所有子应用的写入行为,一律只拦截并写入 fakeWindow,绝对不污染真正的 window
        fakeWindow[prop] = value;
        return true;
      },

      get(target, prop) {
        // 1. 优先从子应用专属的代理容器中提取变量
        if (prop in fakeWindow) {
          return fakeWindow[prop];
        }

        const value = target[prop];

        // 2. 特殊处理:如果是系统方法(如 window.setTimeout/addEventListener),
        // 必须强制绑定为宿主全局 window 的 context,否则执行会报错。
        if (typeof value === 'function') {
          return value.bind(target);
        }

        return value;
      }
    });
  }
}

// 示例测试:
// const sandboxA = new ProxySandbox();
// const sandboxB = new ProxySandbox();
// 
// // 在沙箱 A 代理的作用域内修改变量
// sandboxA.proxy.customVal = 'App A';
// // 在沙箱 B 代理的作用域内修改变量
// sandboxB.proxy.customVal = 'App B';
// 
// console.log(sandboxA.proxy.customVal); // 'App A'
// console.log(sandboxB.proxy.customVal); // 'App B'
// console.log(window.customVal);          // undefined (真实全局 window 毫无污染)

Q4: Webpack HMR(热更新)的工作流与核心原理

HMR(Hot Module Replacement)是指在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面

1. HMR 核心工作流大图景
  1. 文件监听:Webpack Compiler 监听本地文件修改。
  2. 增量编译 :文件变化后,Compiler 进行增量编译,生成两个文件:
    • manifest.json:说明本次更新了哪些模块的 Hash。
    • update.js:本次更新模块的最新代码。
  3. 推送通知 :Webpack Dev Server (WDS) 与浏览器之间维持着一个 WebSocket 长连接。WDS 将最新的编译 Hash 发送给浏览器客户端(HMR Runtime)。
  4. 拉取更新 :HMR Runtime 收到通知后,对比当前版本,通过 Ajax 发起请求拉取最新的 manifest.json,接着通过创建 <script> 标签拉取最新的增量代码 update.js
  5. 模块热插拔 :HMR Runtime 执行最新代码,顺着依赖链向上冒泡 寻找能够接受(accept)此模块热更新的父级模块。若找到,则执行对应回调并替换内存中的模块定义;若一直冒泡到顶层仍无人能接收,则被迫退化为Live Reload(刷新整页)

第三部分:网络与传输(Networking)

Q1: HTTP/1.1、HTTP/2、HTTP/3 的演进及性能改进

协议版本 传输层协议 多路复用 头部压缩 队头阻塞 (HOL Blocking) 问题
HTTP/1.1 TCP ✕ (有管道化但鸡肋) ✕ (纯文本 Header,重复大) 存在(应用层队头阻塞,管道化请求必须按顺序响应)。
HTTP/2 TCP (基于二进制分帧) (HPACK 静态/动态表) 仍存在(TCP 传输层队头阻塞:若某个 TCP 包丢失,会阻塞后续所有流的重传)。
HTTP/3 UDP (基于 QUIC) (连接内各流完全独立) (QPACK,支持乱序解压) 彻底解决(各流之间无干扰,丢失一个数据包仅影响对应的那一条流)。
1. HTTP/2 的核心武器:二进制分帧与多路复用
  • 二进制分帧(Binary Framing):HTTP/2 将通信的最小单位改为二进制的"帧(Frame)"(如 Headers 帧、Data 帧),不再是 HTTP/1 的文本换行。
  • 多路复用(Multiplexing)
    • 在同一个 TCP 连接上,可以并发发送无数个请求和响应。
    • 每个请求/响应被拆分为带有 Stream ID 的帧。
    • 这些帧在 TCP 通道上是交错、乱序发送 的,客户端收到后,通过 Stream ID 将属于同一个请求的帧重组。
    • 这解决了 HTTP/1.1 下浏览器为了并发请求不得不开启 6 个 TCP 连接的尴尬,实现了连接复用
2. HTTP/2 的遗留痛点与 HTTP/3 的 QUIC
  • TCP 协议的队头阻塞
    • TCP 是面向连接、保证可靠传输的协议。在传输层看来,所有的 HTTP/2 流都被塞入同一个 TCP 管道中。
    • 如果其中一个分片包在网络传输中丢失,TCP 必须停下来,等待重传这个丢失的包,才能把后面的数据递交给应用层。
    • 这导致:即使其他 HTTP/2 流的数据早已安全到达,也无法被解包读取,形成了传输层队头阻塞
  • HTTP/3 拥抱 UDP (QUIC)
    • QUIC 协议将可靠传输逻辑移到了应用层,在 UDP 之上实现了连接管理、丢包重传和拥塞控制。
    • 在 QUIC 中,不同的流(Stream)之间是绝对隔离、相互独立的。
    • Stream A 的数据包丢失,只会导致 Stream A 阻塞等待重传;Stream B、C 的传输完全不受任何影响,从而在根本上消除了队头阻塞。

Q2: 强缓存与协商缓存的完整决策流程及常见应用场景

浏览器在发起 HTTP 请求时,会优先检索缓存。缓存分为强缓存协商缓存

1. 强缓存 (Strong Cache)
  • 特点 :浏览器直接从本地缓存中读取数据,不向服务器发起任何网络请求 。在 Chrome Network 中会显示为 from disk cachefrom memory cache,状态码为 200
  • 控制字段
    1. Expires(HTTP/1.0):服务器绝对时间戳(如 Thu, 01 Dec 2026 16:00:00 GMT)。缺点是若客户端本地时间被修改,会导致缓存失效。
    2. Cache-Control(HTTP/1.1,优先级最高 ):相对时间跨度,单位秒(如 max-age=31536000)。常见可选值有:
      • no-store:绝对不允许缓存任何数据。
      • no-cache:可以缓存,但每次使用前必须先向服务器发起请求进行协商缓存校验
      • public / private:是否允许中间代理服务器(如 CDN)缓存。
2. 协商缓存 (Validation Cache)
  • 特点 :当强缓存失效(超时)后,浏览器携带缓存标识向服务器发起请求,由服务器决定当前缓存是否依然有效。
    • 若有效:服务器返回 304 Not Modified,不返回响应体,浏览器直接使用本地缓存。
    • 若失效:服务器返回 200 OK,并返回最新的响应体和新的缓存标识。
  • 控制字段对
    • 第一对(基于时间戳)Last-Modified(响应头,服务器给出)与 If-Modified-Since(请求头,浏览器携带)。
      • 缺点:时间戳精确度仅为秒级;且如果文件在 1 秒内被修改,或者文件只是被编辑了但内容没变,这种机制会发生误判。
    • 第二对(基于内容哈希 - 优先级更高)ETag(响应头,唯一资源指纹)与 If-None-Match(请求头,浏览器携带)。
      • 优点:只要文件内容变动,ETag 哈希值必然改变,精准度极高。
【协商缓存决策流程图解】
text 复制代码
                  [ 发起 HTTP 请求 ]
                         │
               是否命中强缓存(max-age)?
               /                 \
            (是)                 (否)
             /                     \
    [ 200 from cache ]      携带缓存标识(If-None-Match/If-Modified-Since)
   (不与服务器发生任何通信)         向服务器发起 HTTP 请求
                                     │
                             服务器比对标识是否一致?
                             /                 \
                          (是)                 (否)
                           /                     \
                  [ 304 Not Modified ]      [ 200 OK + 最新数据 ]
                  (快速返回,复用旧缓存)     (传输新内容,更新缓存标识)
3. 最佳实践部署策略
  • 静态资源(HTML 除外,如 JS, CSS, 图片)
    • 方案 :开启超长强缓存:Cache-Control: max-age=31536000
    • 更新机制 :在打包工程中,采用**内容哈希(ContentHash)**作为文件名(如 index.a8bc93.js)。一旦代码改变,文件名改变,等同于请求一个全新资源,完美避开强缓存陷阱。
  • 单页应用 HTML 入口文件(如 index.html)
    • 方案 :设置无强缓存,但走协商缓存:Cache-Control: no-cache
    • 原因 :必须保证每次用户打开页面,浏览器都要去服务器校验一次 index.html 是否有更新,以确保用户能第一时间加载到带有最新 ContentHash 的 JS 静态文件。

Q3: HTTPS 的加密握手及秘钥协商完整过程

HTTPS 的核心是 TLS/SSL 协议。为了兼顾安全性 (非对称加密安全性高,但计算极慢)与性能 (对称加密计算快,但秘钥无法在明文通道安全传输),HTTPS 采用了混合加密机制。

1. 核心流程步骤拆解
  1. Client Hello(客户端握手请求)
    • 客户端向服务端发送自己支持的 TLS 版本、加密套件列表(Cipher Suites)、以及一个客户端随机数 (Client Random)
  2. Server Hello(服务端应答)
    • 服务端确认 TLS 版本、选择一套加密套件。
    • 向客户端发送一个服务端随机数 (Server Random)
    • 向客户端发送数字证书(Certificate) (内含服务端的 非对称公钥)。
  3. 证书校验
    • 客户端读取证书,顺着证书链向内置在操作系统/浏览器中的受信任根证书颁发机构 (CA) 发起数字签名校验,确保证书真实合法,防止中间人攻击。
  4. Pre-Master Secret 传输
    • 证书校验无误后,客户端在本地生成第三个随机数:预主秘钥 (Pre-Master Secret)
    • 客户端使用证书中的服务端公钥Pre-Master Secret 进行加密,并发送给服务端。
  5. 生成会话秘钥(Session Key)
    • 服务端收到密文,使用自己绝对保密的非对称私钥 解密,得到 Pre-Master Secret
    • 此时,客户端与服务端都同时拥有了:Client Random + Server Random + Pre-Master Secret
    • 双方各自利用这三个随机数,通过约定的算法计算出完全一致的会话秘钥(Session Key)
  6. 握手完毕与对称通信
    • 双方互相发送 Finished 报文,声明后续所有的应用层 HTTP 数据传输都将全部采用 Session Key(对称加密) 进行加解密。

Q4: WebRTC 的 P2P 连接建立过程(信令与打洞)

WebRTC 允许浏览器之间直接进行端到端(P2P)的音视频和数据传输。但由于防火墙 and NAT(网络地址转换)的存在,两个浏览器很难直接获知对方的真实 IP 从而建立连接。这就需要一套复杂的信令协商打洞机制。

1. 核心概念释义
  • 信令服务器(Signaling Server):双端在建立 P2P 前,用于中转媒体配置信息(SDP)和网络候选地址(ICE Candidate)的公共服务器(通常基于 WebSocket 实现)。
  • SDP(Session Description Protocol - 会话描述协议):一种文本格式,包含本端支持的音视频编码格式、分辨率、以及其他媒体协商参数。
  • STUN 服务器:用于帮助处于 NAT 后方的客户端查询自己映射后的公网 IP 和端口号。
  • TURN 服务器 :当 NAT 限制极度严格(对称型 NAT),双端无论如何打洞都无法直接互通时,充当流量中转服务器(此时 P2P 退化为中转,但传输依旧受 TLS 加密保护)。
【结构化伪代码:WebRTC 双端 P2P 建立的核心逻辑】
javascript 复制代码
// 假设双端为 呼叫端(Caller) 和 接收端(Callee)
// 双端通过 WebSocket信令通道 进行数据中转

/* ---------------- 呼叫端 (Caller) 逻辑 ---------------- */
函数 发起呼叫() {
  // 1. 创建本地 PeerConnection 连接对象
  本地连接 = 创建 RTCPeerConnection({
    ice服务器配置列表: [{ url: "stun:stun.l.google.com:19302" }]
  })

  // 2. 监听 ICE 候选地址生成
  本地连接.当收集到ICE候选地址 = (事件) => {
    如果 (事件.候选地址存在) {
      通过信令通道发送给接收端("candidate", 事件.候选地址)
    }
  }

  // 3. 创建 SDP 提议 (Offer)
  提议SDP = 异步 本地连接.创建提议()
  异步 本地连接.设置本地描述(提议SDP)

  // 4. 将提议 SDP 通过信令服务器发送给接收端
  通过信令通道发送给接收端("offer", 提议SDP)
}

当信令通道收到应答(应答SDP) {
  // 5. 写入接收端的回应 SDP
  异步 本地连接.设置远端描述(应答SDP)
}


/* ---------------- 接收端 (Callee) 逻辑 ---------------- */
当信令通道收到提议(提议SDP) {
  // 1. 创建自己的 PeerConnection 对象
  接收端连接 = 创建 RTCPeerConnection({
    ice服务器配置列表: [{ url: "stun:stun.l.google.com:19302" }]
  })

  // 2. 监听并收集自己的 ICE 候选地址发送给呼叫端
  接收端连接.当收集到ICE候选地址 = (事件) => {
    如果 (事件.候选地址存在) {
      通过信令通道发送给呼叫端("candidate", 事件.候选地址)
    }
  }

  // 3. 写入呼叫端的提议 SDP
  异步 接收端连接.设置远端描述(提议SDP)

  // 4. 创建 SDP 应答 (Answer)
  应答SDP = 异步 接收端连接.创建应答()
  异步 接收端连接.设置本地描述(应答SDP)

  // 5. 回复应答 SDP
  通过信令通道发送给呼叫端("answer", 应答SDP)
}


/* ---------------- 双端通用网络协商 (ICE Candidate) ---------------- */
当任何一端从信令通道收到("candidate", 候选地址) {
  // 将对方的网络候选通路塞入连接中,WebRTC 会在后台自动尝试打洞测试联通性
  异步 本端连接.添加远端候选地址(候选地址)
}
【WebRTC 双端 P2P 建立的核心流程】
javascript 复制代码
// 真实业务中,双端通过 WebSocket 信令通道交换消息。这里定义底层的 WebRTC 连接类。

// 1. 呼叫端 (Caller)
class CallerPC {
  constructor(signaling) {
    this.signaling = signaling; // 外部传入的信令通信实例
    this.pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // 免费 STUN 服务器用于获取公网 IP
    });

    // 监听本地 ICE 候选地址的生成并发送给接收端
    this.pc.onicecandidate = (event) => {
      if (event.candidate) {
        this.signaling.send(JSON.stringify({ type: 'candidate', data: event.candidate }));
      }
    };
  }

  // 发起呼叫,创建 SDP Offer 提议
  async startCall() {
    const offer = await this.pc.createOffer();
    await this.pc.setLocalDescription(offer);
    this.signaling.send(JSON.stringify({ type: 'offer', data: offer }));
  }

  // 写入远端的 SDP Answer 应答
  async handleAnswer(answer) {
    await this.pc.setRemoteDescription(new RTCSessionDescription(answer));
  }

  // 写入远端的 ICE 候选通道,开始底层打洞连接
  async handleRemoteCandidate(candidate) {
    await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
  }
}

// 2. 接收端 (Callee)
class CalleePC {
  constructor(signaling) {
    this.signaling = signaling;
    this.pc = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
    });

    this.pc.onicecandidate = (event) => {
      if (event.candidate) {
        this.signaling.send(JSON.stringify({ type: 'candidate', data: event.candidate }));
      }
    };
  }

  // 写入呼叫端的 SDP Offer,并创建回应的 SDP Answer
  async handleOffer(offer) {
    await this.pc.setRemoteDescription(new RTCSessionDescription(offer));
    const answer = await this.pc.createAnswer();
    await this.pc.setLocalDescription(answer);
    this.signaling.send(JSON.stringify({ type: 'answer', data: answer }));
  }

  async handleRemoteCandidate(candidate) {
    await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
  }
}
相关推荐
夜白宋1 小时前
【Redis深入】二、高性能
java·前端·redis
Multipath7121 小时前
多卡多链路聚合路由器的原理、关键技术分析
网络·5g·安全·智能路由器·无人机·实时音视频
MAXrxc1 小时前
BGP小作业
网络
飞函安全1 小时前
石油化工企业园区面积大、网络复杂,飞函如何保证跨区域协同不掉线
网络·安全·私有化im
梦奇不是胖猫1 小时前
[ 计算机网络 | 第四章 ] 网络层 03 如何选择路径?
网络·计算机网络·智能路由器
艾莉丝努力练剑1 小时前
【Linux网络】传输层协议TCP(六)补充 - 面试题:HTTP 获取网页的完整过程
linux·运维·网络·tcp/ip·计算机网络·http·udp
nnsix1 小时前
Unity 自定义包的 package.json 简单写法
java·服务器·前端
minji...1 小时前
Linux高级IO(六)基于ET模式、单reactor反应堆的epoll版本的TCP计算服务器
linux·服务器·网络·c++·epoll·socket套接字·reactor反应堆模式
书中枫叶1 小时前
生活缴费充值系统
前端·javascript·经验分享·mongodb·node.js