二面考察
考点二:SPA(单页应用)与 MPA(多页应用)的区别
- 基础认知:
- SPA:只有一个 HTML 页面(
index.html),路由切换由 JS 控制(动态替换 DOM)。如 Vue/React 项目。 - MPA:多个独立 HTML 页面,每次跳转都是向服务器请求新页面。如传统 jQuery 项目。
- SPA:只有一个 HTML 页面(
- 进阶原理(优劣势对比):
| 维度 | SPA | MPA |
|---|---|---|
| 首屏加载 | 慢(需加载大 JS 包) | 快(只加载当前页面资源) |
| 页面切换 | 极快(无刷新,只换 DOM) | 慢(白屏等待,重新加载所有资源) |
| SEO(搜索引擎) | 差(需 SSR 或预渲染补救) | 好(服务端直接返回完整 HTML) |
| 前后端分离 | 彻底分离 | 耦合较高 |
实战结合(你的简历):
“在海嵘的三个平台(IRMP/MMMP/IAMP) ,我全选了 SPA 架构,因为它们是强交互的后台管理系统,用户体验流畅更重要。但为了优化首屏,我做了路由懒加载 + CDN 加速。而在智慧工地早期项目(天津四方),由于需要兼顾 SEO(施工方案展示),我们部分页面用了 MPA + 服务端渲染(SSR) 混合模式。”
考点三:地址栏输入 URL 敲下回车后发生了什么(必考经典)
基础认知(五步走):DNS 解析 -> TCP 连接(三次握手) -> 发送 HTTP 请求 -> 服务器处理并返回 -> 浏览器解析渲染。
进阶原理(渲染流水线 - 关键渲染路径):
- DNS 解析:将域名转成 IP(查浏览器缓存 -> 系统 Hosts -> 本地 DNS -> 根 DNS)。
- TCP 三次握手:建立可靠连接(SYN -> SYN-ACK -> ACK)。如果是 HTTPS,还有 TLS 四次握手(加密通道建立)。
- 发送请求:浏览器构建 HTTP 报文(请求行、请求头、请求体)。
- 服务器响应:返回 HTML 文本。
- 浏览器解析(重点!):
- HTML 解析器将 HTML 转成 DOM 树。
- CSS 解析器将 CSS 转成 CSSOM 树(注意:CSS 加载阻塞渲染,但不阻塞 HTML 解析)。
- 两者结合生成 渲染树(Render Tree)(
display:none不会进入渲染树)。 - 布局(Layout/Reflow):计算每个节点的几何位置(宽高、坐标)。
- 绘制(Paint):将像素绘制到屏幕上。
- 合成(Composite):将多个图层合并输出(GPU 加速)。
实战结合(你如何优化这个过程):
“我在优化 IRMP 首屏时,重点关注了关键渲染路径。具体做法是:
- DNS 预解析:
<link rel="dns-prefetch" href="//api.xxx.com">,提前解析接口域名。 - 阻塞优化:将非首屏 JS 加上
async或defer(async不阻塞 HTML 解析,但加载完即执行;defer在 DOM 解析完后执行)。 - 内联关键 CSS:将首屏必须的样式(如骨架屏)直接内联在
<head>,避免 CSS 阻塞渲染导致白屏。”
- DNS 预解析:
考点四:减少回流(Reflow)与重绘(Repaint)+ HTTP/2.0 vs 1.1
第一问:如何减少回流重绘?
基础认知:
- 回流(Reflow):几何属性(宽高、位置、字体)变了,浏览器重新计算布局。开销极大。
- 重绘(Repaint):外观属性(颜色、背景)变了,只重新绘制像素。开销较小。回流必导致重绘。
进阶优化手段(背下来):
- 合并 DOM 操作:用
document.createDocumentFragment()批量添加节点,一次性插入(只触发一次回流)。 - 读写分离:访问几何属性(
offsetTop、scrollHeight)会强制浏览器刷新队列(回流)。统一先读后写。 - 脱离文档流:用
display: none暂存元素,修改完再显示(触发两次回流,但比多次小修改好)。 - 使用 CSS3 硬件加速:
transform、opacity、filter触发的合成(Composite),直接交给 GPU,不触发回流/重绘(最推荐)。
- 合并 DOM 操作:用
代码示例(读写分离):
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)。
- 1.1:线头阻塞(Head-of-Line Blocking),同一连接只能串行发请求,虽支持
- 实战结合(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 的两大痛点):
- 解决 TCP 队头阻塞:TCP 丢包重传会阻塞后续所有流。QUIC 独自分流,一个流丢包,其他流照常跑。
- 0-RTT 连接建立:第一次连接 1-RTT,之后复用连接可实现 0-RTT(不用再握手),极大提升弱网下的加载速度。
- 连接迁移:手机从 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 美观,无#号。
- Hash 模式:
- 进阶原理(服务端必配):
- History 模式最大的坑:刷新页面时,浏览器会向
/dashboard发请求,如果服务器没有配置,返回 404。 - 解决方案(Nginx 配置):
try_files $uri $uri/ /index.html;(所有路径都指向index.html,让前端路由接管)。
- History 模式最大的坑:刷新页面时,浏览器会向
- 实战结合(你的 IRMP):
“在 IRMP 项目,我们选了 History 模式,因为报告链接需要分享给客户(带
#的链接有时会被邮件识别截断,且不够专业)。部署时,我在 Nginx 加入了try_files回退规则。如果用户直接访问/report/123,Nginx 会返回index.html,前端路由解析后展示对应报告详情页。”
考点七:手撕代码 —— LRU Cache(最近最少使用缓存)
面试官潜台词:考察你对 Map 数据结构(哈希表)和链表(顺序性)的综合运用。要求 get 和 put 时间复杂度 O(1)。
- 基础认知:缓存满了,优先淘汰最久没被访问的那个。比如 Redis 内存淘汰策略。
- 进阶解法(JS 中利用 Map 特性作弊法 + 标准双向链表法)
解法一:利用 JS 原生 Map 的有序特性(最简 MVP,面试可写)
原理:
Map迭代顺序就是插入顺序。get时如果存在,先删除再重新 set,让它回到最新位置(尾部)。
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 特性,自己实现数据结构”,用这个:
// 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),这样在插入和删除时不需要判空,代码更健壮。
get和put全部操作都基于哈希表 O(1) 定位 + 链表 O(1) 移动,完全符合题目要求。”