Skip to content

二面考察

考点二:SPA(单页应用)与 MPA(多页应用)的区别

  • 基础认知
    • SPA:只有一个 HTML 页面(index.html),路由切换由 JS 控制(动态替换 DOM)。如 Vue/React 项目。
    • MPA:多个独立 HTML 页面,每次跳转都是向服务器请求新页面。如传统 jQuery 项目。
  • 进阶原理(优劣势对比)
维度SPAMPA
首屏加载慢(需加载大 JS 包)快(只加载当前页面资源)
页面切换极快(无刷新,只换 DOM)慢(白屏等待,重新加载所有资源)
SEO(搜索引擎)差(需 SSR 或预渲染补救)好(服务端直接返回完整 HTML)
前后端分离彻底分离耦合较高
  • 实战结合(你的简历)

    “在海嵘的三个平台(IRMP/MMMP/IAMP) ,我全选了 SPA 架构,因为它们是强交互的后台管理系统,用户体验流畅更重要。但为了优化首屏,我做了路由懒加载 + CDN 加速。而在智慧工地早期项目(天津四方),由于需要兼顾 SEO(施工方案展示),我们部分页面用了 MPA + 服务端渲染(SSR) 混合模式。”


考点三:地址栏输入 URL 敲下回车后发生了什么(必考经典)

  • 基础认知(五步走):DNS 解析 -> TCP 连接(三次握手) -> 发送 HTTP 请求 -> 服务器处理并返回 -> 浏览器解析渲染。

  • 进阶原理(渲染流水线 - 关键渲染路径)

    1. DNS 解析:将域名转成 IP(查浏览器缓存 -> 系统 Hosts -> 本地 DNS -> 根 DNS)。
    2. TCP 三次握手:建立可靠连接(SYN -> SYN-ACK -> ACK)。如果是 HTTPS,还有 TLS 四次握手(加密通道建立)。
    3. 发送请求:浏览器构建 HTTP 报文(请求行、请求头、请求体)。
    4. 服务器响应:返回 HTML 文本。
    5. 浏览器解析(重点!)
      • HTML 解析器将 HTML 转成 DOM 树
      • CSS 解析器将 CSS 转成 CSSOM 树注意:CSS 加载阻塞渲染,但不阻塞 HTML 解析)。
      • 两者结合生成 渲染树(Render Tree)display:none 不会进入渲染树)。
      • 布局(Layout/Reflow):计算每个节点的几何位置(宽高、坐标)。
      • 绘制(Paint):将像素绘制到屏幕上。
      • 合成(Composite):将多个图层合并输出(GPU 加速)。
  • 实战结合(你如何优化这个过程)

    “我在优化 IRMP 首屏时,重点关注了关键渲染路径。具体做法是:

    1. DNS 预解析<link rel="dns-prefetch" href="//api.xxx.com">,提前解析接口域名。
    2. 阻塞优化:将非首屏 JS 加上 asyncdeferasync 不阻塞 HTML 解析,但加载完即执行;defer 在 DOM 解析完后执行)。
    3. 内联关键 CSS:将首屏必须的样式(如骨架屏)直接内联在 <head>,避免 CSS 阻塞渲染导致白屏。”

考点四:减少回流(Reflow)与重绘(Repaint)+ HTTP/2.0 vs 1.1

第一问:如何减少回流重绘?

  • 基础认知

    • 回流(Reflow):几何属性(宽高、位置、字体)变了,浏览器重新计算布局。开销极大
    • 重绘(Repaint):外观属性(颜色、背景)变了,只重新绘制像素。开销较小。回流必导致重绘
  • 进阶优化手段(背下来)

    1. 合并 DOM 操作:用 document.createDocumentFragment() 批量添加节点,一次性插入(只触发一次回流)。
    2. 读写分离:访问几何属性(offsetTopscrollHeight)会强制浏览器刷新队列(回流)。统一先读后写
    3. 脱离文档流:用 display: none 暂存元素,修改完再显示(触发两次回流,但比多次小修改好)。
    4. 使用 CSS3 硬件加速transformopacityfilter 触发的合成(Composite),直接交给 GPU,不触发回流/重绘(最推荐)。
  • 代码示例(读写分离)

    javascript
    // ❌ 错误写法:读一次,写一次,强刷队列 4 次
    box.style.left = box.offsetLeft + 10 + 'px';
    box.style.top = box.offsetTop + 10 + 'px';

// ✅ 正确写法:先读,后写(只触发 1 次回流) const left = box.offsetLeft; const top = box.offsetTop; box.style.left = left + 10 + 'px'; box.style.top = top + 10 + 'px';

第二问:HTTP/2.0 和 1.1 的区别?

  • 基础区别
    • 1.1线头阻塞(Head-of-Line Blocking),同一连接只能串行发请求,虽支持 Keep-Alive,但前面的请求慢,后面的得等着。文本协议(可读性高,但效率低)。
    • 2.0二进制分帧(将数据拆成帧,乱序传输)、多路复用(Multiplexing)(一条 TCP 连接并发无数请求,互不干扰)、头部压缩(HPACK)(减少重复 Header)、服务器推送(Server Push)(主动推 CSS/JS)。
  • 实战结合(IAMP 点云加载优化)

    “在 IAMP 平台,我们曾经加载 100 个小图片切片,HTTP/1.1 下浏览器限制同一域名最多 6 个并发,排队严重。启用 HTTP/2 后,单个 TCP 连接并发传输,加载时间直接减半。另外,我们用到了 HTTP/2 的服务器推送,在请求 index.html 时,Nginx 主动推了首屏必须的 three.min.js,省去了浏览器解析 HTML 后再去请求 JS 的 RTT(往返时延)。”


考点五:QUIC 协议(加分项,体现技术视野)

  • 基础认知:Google 开发的基于 UDP 的传输层协议,是 HTTP/3 的底层核心
  • 进阶原理(解决 TCP 的两大痛点)
    1. 解决 TCP 队头阻塞:TCP 丢包重传会阻塞后续所有流。QUIC 独自分流,一个流丢包,其他流照常跑
    2. 0-RTT 连接建立:第一次连接 1-RTT,之后复用连接可实现 0-RTT(不用再握手),极大提升弱网下的加载速度。
    3. 连接迁移:手机从 WiFi 切 5G,IP 变了,TCP 得重连;QUIC 用 Connection ID 识别,连接不中断(超级适合移动端)。
  • 面试话术(谦虚且加分)

    “面试官,QUIC 我了解过基本原理。虽然目前 WeLink 和 IAMP 还未切到 HTTP/3,但我在性能预研时关注过它。特别是针对移动端隧道检测车(弱网环境),QUIC 的 0-RTT 和连接迁移特性理论上能显著提升弱网下的数据上报成功率。我正计划在下一期项目中尝试落地。”


考点六:History 与 Hash 路由模式

  • 基础认知
    • Hash 模式https://site.com/#/dashboard。通过 window.onhashchange 监听 # 后的变化。兼容性好(支持 IE),改变 hash 浏览器不会发送请求,纯前端控制。
    • History 模式https://site.com/dashboard。利用 History.pushState()popstate 事件。URL 美观,无 # 号。
  • 进阶原理(服务端必配)
    • History 模式最大的坑:刷新页面时,浏览器会向 /dashboard 发请求,如果服务器没有配置,返回 404。
    • 解决方案(Nginx 配置):try_files $uri $uri/ /index.html;(所有路径都指向 index.html,让前端路由接管)。
  • 实战结合(你的 IRMP)

    “在 IRMP 项目,我们选了 History 模式,因为报告链接需要分享给客户(带 # 的链接有时会被邮件识别截断,且不够专业)。部署时,我在 Nginx 加入了 try_files 回退规则。如果用户直接访问 /report/123,Nginx 会返回 index.html,前端路由解析后展示对应报告详情页。”


考点七:手撕代码 —— LRU Cache(最近最少使用缓存)

面试官潜台词:考察你对 Map 数据结构(哈希表)和链表(顺序性)的综合运用。要求 getput 时间复杂度 O(1)

  • 基础认知:缓存满了,优先淘汰最久没被访问的那个。比如 Redis 内存淘汰策略。
  • 进阶解法(JS 中利用 Map 特性作弊法 + 标准双向链表法)

解法一:利用 JS 原生 Map 的有序特性(最简 MVP,面试可写)

原理Map 迭代顺序就是插入顺序。get 时如果存在,先删除再重新 set,让它回到最新位置(尾部)。

javascript
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map(); // 利用 Map 的有序性
  }

  get(key) {
    if (!this.cache.has(key)) return -1;
    // 【核心】更新访问顺序:删除再重新插入,移到尾部(最新)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }

  put(key, value) {
    // 如果 key 存在,先删掉(后续重新插入变最新)
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    // 如果满了,删除最旧的那个(头部)
    if (this.cache.size >= this.capacity) {
      // Map 的 keys() 迭代器,.next().value 拿到第一个 key
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
  }
}

解法二:标准双向链表 + 哈希表(手写链表节点,展示硬核功底)

如果面试官说“不能用 Map 特性,自己实现数据结构”,用这个:

javascript
// 1. 定义双向链表节点
class Node {
  constructor(key, value) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map(); // 存 key -> Node 映射,O(1) 取节点
    // 哨兵节点(dummy head/tail),避免空指针判断
    this.head = new Node(0, 0);
    this.tail = new Node(0, 0);
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  // ---- 辅助函数:将节点移到尾部(代表最新) ----
  _moveToTail(node) {
    this._removeNode(node);
    this._addToTail(node);
  }

  // ---- 辅助函数:删除节点 ----
  _removeNode(node) {
    node.prev.next = node.next;
    node.next.prev = node.prev;
  }

  // ---- 辅助函数:插入到尾部 ----
  _addToTail(node) {
    const last = this.tail.prev;
    last.next = node;
    node.prev = last;
    node.next = this.tail;
    this.tail.prev = node;
  }

  // ---- 核心 get ----
  get(key) {
    if (!this.map.has(key)) return -1;
    const node = this.map.get(key);
    this._moveToTail(node); // 访问了,移到尾部(最新)
    return node.value;
  }

  // ---- 核心 put ----
  put(key, value) {
    if (this.map.has(key)) {
      const node = this.map.get(key);
      node.value = value; // 更新值
      this._moveToTail(node); // 移到尾部
    } else {
      const newNode = new Node(key, value);
      this.map.set(key, newNode);
      this._addToTail(newNode);

      if (this.map.size > this.capacity) {
        // 移除头部节点的下一个(最久未使用)
        const firstNode = this.head.next;
        this._removeNode(firstNode);
        this.map.delete(firstNode.key);
      }
    }
  }
}

// 测试
const lru = new LRUCache(2);
lru.put(1, 1);
lru.put(2, 2);
console.log(lru.get(1)); // 1
lru.put(3, 3); // 淘汰 key 2
console.log(lru.get(2)); // -1

面试时主动补充:“我用了哨兵节点(Dummy Head/Tail),这样在插入和删除时不需要判空,代码更健壮。getput 全部操作都基于哈希表 O(1) 定位 + 链表 O(1) 移动,完全符合题目要求。”

最近更新