字节-前端开发-Cross Platform-一面
字节跳动|前端开发|Cross Platform|一面
这一轮前半部分主要围绕 WebSocket、TCP、Electron 多进程、React 流式渲染展开,中间包含一道允许使用 AI 的 Coding 题。后半部分主要围绕 AI Coding 的实际开发方式连续追问,包括 Rules、Context、Memory、AGENTS.md、Spec 和 Multi-Agent。
1. WebSocket 的连接建立和管理
WebSocket 建立在 TCP 之上。对于 ws://,首先建立 TCP 连接,然后客户端发送一个 HTTP Upgrade 请求,请求从 HTTP 协议升级到 WebSocket。如果使用 wss://,则会在 TCP 建连之后先进行 TLS 握手,再执行 WebSocket Upgrade。
升级完成以后,双方在同一个 TCP 连接上进行全双工通信,不再遵循普通 HTTP 的 Request / Response 模式。
工程上 WebSocket 的管理还包括连接状态维护、心跳、断线重连、网络恢复、页面生命周期管理以及消息幂等。
WebSocket 协议本身定义了 Ping / Pong,但是浏览器 WebSocket API 并不直接开放发送协议级 Ping 的能力,因此 Web 前端通常还会实现应用层心跳。
重连时通常使用指数退避,避免服务器异常时客户端同时高频重连。如果业务要求较高,还可以在应用层加入 messageId、sequence 和 ACK,解决重复消息、业务确认等问题。
2. 网络抖动导致丢包时,WebSocket 和 TCP 会发生什么变化
WebSocket 本身不负责网络层丢包恢复,可靠传输由下面的 TCP 完成。
TCP 数据发生丢失后,TCP 会通过快速重传或者超时重传恢复数据,同时拥塞控制算法可能降低发送速率。
由于 TCP 提供可靠、有序的字节流,如果前面的数据没有恢复,即使后面的数据已经到达,上层也无法正常按照顺序消费,这就是 TCP 的 Head-of-Line Blocking。
所以轻微网络抖动时,WebSocket 通常不会立刻断开,而是表现为消息延迟增大。
只有 TCP 长时间无法恢复时,WebSocket 才会最终表现为连接异常或者关闭,此时需要应用层重新建立连接。
3. TCP Server 如何知道发生了丢包
首先要区分当前 Server 是数据发送方还是接收方。
TCP 使用 Sequence Number 标记字节流的位置。接收端可以通过序列号发现收到的数据存在缺口。
例如发送方依次发送:
SEQ = 1000
SEQ = 2000
SEQ = 3000
如果 SEQ=2000 对应的数据丢失,而后面的数据已经到达,接收端会继续 ACK 自己下一步期待收到的位置。
ACK = 2000
ACK = 2000
ACK = 2000
发送端收到多个 Duplicate ACK 后,可以推断中间某段数据可能丢失,并触发 Fast Retransmit。
现代 TCP 还可以通过 SACK,也就是 Selective Acknowledgement,让接收端告诉发送端哪些不连续的数据区间已经收到,从而只重传真正缺失的部分。
如果丢失发生在尾部,没有足够的后续 Segment 产生重复 ACK,则主要依靠 RTO,也就是 Retransmission Timeout。一定时间没有收到确认以后,发送端触发超时重传。
因此真正决定进行重传的通常是 TCP 发送端。
4. ping 一个目标主机的完整过程是什么?使用了什么协议?
Ping 主要使用的是 ICMP,也就是 Internet Control Message Protocol。它不基于 TCP 或 UDP,而是直接封装在 IP 数据报中,用来进行网络控制和差错报告。ping 使用的核心报文是 ICMP Echo Request 和 ICMP Echo Reply。
如果我执行 ping example.com,首先如果目标是域名,系统会通过 DNS 将域名解析成目标 IP。拿到 IP 以后,操作系统会查询本机路由表,判断这个数据包应该直接发送给目标主机,还是发送给默认网关。
如果目标位于同一个局域网,本机会将数据直接交给目标主机;如果目标不在本地网段,则会发送给下一跳网关。在 IPv4 中,本机需要知道下一跳对应的 MAC 地址,因此会先查询 ARP 缓存。如果缓存不存在,就发送 ARP Request 获取目标主机或者网关的 MAC 地址。IPv6 中对应的机制是 NDP,而不是 ARP。
之后操作系统构造一个 ICMP Echo Request。ICMP 报文中包含 Type、Code、Checksum、Identifier 和 Sequence Number 等字段,然后 ICMP 报文会被封装到 IP 数据报中,再封装成链路层帧发送出去。
整个协议封装关系可以理解为:
Ethernet / Wi-Fi
↓
IP
↓
ICMP Echo Request
数据包经过中间路由器时,路由器根据目标 IP 查路由表进行转发,同时 IP Header 中的 TTL 每经过一跳减 1。如果 TTL 减到 0,当前路由器会丢弃数据包,并通常向源主机返回 ICMP Time Exceeded。
当目标主机收到 ICMP Echo Request 后,如果它允许响应 ICMP,就会生成对应的 ICMP Echo Reply,并按照 IP 路由返回给源主机。
本机收到 Echo Reply 后,会根据 Identifier 和 Sequence Number 匹配对应的请求,并利用发送时间和接收时间计算 RTT,也就是 Round Trip Time。连续发送多个 ICMP 请求以后,还可以统计丢包率、平均 RTT 等信息。
Ping 不通可能是网络确实不可达,也可能只是目标主机、防火墙或者中间设备禁止了 ICMP Echo Request。此时 HTTP、TCP 等业务连接仍然可能是正常的。
Ping 本身不建立 TCP 连接,也没有三次握手。ICMP 是网络层相关的控制协议,直接封装在 IP 中。只有像 telnet host port、nc 或 TCP SYN 探测这类工具才是在测试 TCP 端口连通性。
Ping 的核心过程是先通过 DNS 获取目标 IP,再根据路由表选择下一跳,通过 ARP 或 NDP 获取链路层地址,然后发送 ICMP Echo Request。数据包经 IP 路由到达目标主机,目标返回 ICMP Echo Reply,本机根据响应统计 RTT 和丢包情况。Ping 使用的是 ICMP,而不是 TCP 或 UDP。
5. Electron 的多窗口架构是什么样的
Electron 基于 Chromium 的多进程架构,同时提供 Node.js 能力。
一个 Electron 应用通常只有一个 Main Process。Main Process 负责应用生命周期、窗口创建、系统 API、菜单、Tray、协议注册以及 IPC 调度。
每一个 BrowserWindow 对应一个 webContents,通常运行在独立的 Renderer Process 中。React、Vue 等页面代码主要运行在 Renderer 中。
现代 Electron 一般开启 contextIsolation,页面代码不直接访问 Node.js,而是通过 preload 中的 contextBridge 暴露受限接口,再使用 IPC 与 Main Process 通信。
除 Main 和 Renderer 外,Electron / Chromium 内部还可能存在 GPU Process、Utility Process 等其他进程,所以 Electron 并不是简单的双进程模型。
6. Main 进程负责什么逻辑?Node 进程负责什么逻辑?大计算量任务放在哪里?如何避免 Main 卡顿?
Electron 的 Main Process 本身运行在 Node.js 环境中,因此 Main Process 和所谓的 Node Process 并不是天然分开的两个概念。
Main Process 主要负责窗口生命周期、应用生命周期、系统能力、Native API、菜单、Tray、文件系统权限以及 IPC 调度。
如果这里所说的 Node 进程指额外启动的 Node.js 子进程,那么可以用于运行独立后台服务或者计算任务。
Main Process 不应该执行长时间 CPU 密集型任务,因为它依赖自己的 Event Loop。如果主线程长期被占用,Main Process 无法及时处理 IPC、窗口管理和其他事件。
CPU 密集型任务可以根据场景放到:
- Node.js
worker_threads - Electron
UtilityProcess child_process- Renderer 中的 Web Worker
对于 Node.js 内部纯 CPU 计算,worker_threads 比较合适。需要更强隔离时可以使用独立进程。Renderer 内的纯前端计算则可以使用 Web Worker。
核心原则是 Main Process 负责控制和调度,而不是执行耗时计算。
7. 你刚刚提到了 Workers,可以展开讲讲吗
Electron 中要区分浏览器的 Web Worker 和 Node.js 的 worker_threads。
Web Worker 运行在 Renderer 侧,拥有独立线程,可以执行 CPU 密集型 JavaScript,但不能直接操作 DOM。
worker_threads 是 Node.js 提供的多线程能力。每个 Worker 拥有独立的 V8 Isolate 和 Event Loop,可以执行 Node.js 代码。
线程之间通常通过 postMessage 通信。普通 JavaScript 对象采用 Structured Clone,因此大量数据频繁在线程之间传递仍然存在复制成本。
对于 ArrayBuffer 等数据,可以通过 Transferable Object 转移所有权,避免数据复制。需要共享内存时,还可以使用 SharedArrayBuffer 和 Atomics。
持续存在的大量计算任务一般不会频繁创建和销毁 Worker,而是维护 Worker Pool,将任务分发给固定数量的 Worker。
8. Main Process 卡顿以后,对 Renderer 有影响吗
Renderer 和 Main Process 通常运行在不同的操作系统进程中,因此 Main Process 被 CPU 密集型任务阻塞时,并不意味着 Renderer 的 JavaScript 和页面渲染也会立即停止。
如果页面中的逻辑完全运行在 Renderer 内部,那么按钮点击、输入框编辑、React State 更新、动画等操作仍然可以继续执行。
但是所有依赖 Main Process 的能力都会受到影响。
例如 Renderer 发送 IPC 请求:
await window.electronAPI.readFile();
如果这个调用最终需要 Main Process 处理,而 Main Process 的 Event Loop 已经被阻塞,那么对应 IPC 请求就无法及时得到响应。
因此可能出现这样一种情况:
同时,窗口创建、关闭、系统菜单、Tray、文件系统调用以及依赖 Main 的 Native 能力也可能失去响应。
如果 Renderer 使用同步 IPC,例如 ipcRenderer.sendSync,情况更严重。因为 Renderer 会同步等待 Main Process 返回结果,此时 Renderer 自身也会一起卡住。
所以更准确的结论是:
Main Process 卡顿不会天然阻塞 Renderer 自身的渲染和 JavaScript 执行,但是所有依赖 Main 的 IPC 和系统能力都会阻塞。如果 Renderer 同步等待 Main,那么 Renderer 也会表现为卡死。
9. SSE Token 流,每个 Token 都进行 setState,会有什么影响
如果 SSE 每收到一个 Token 就执行:
setContent((prev) => prev + token);
可能导致非常高频的 React State Update。
State 更新可能触发组件重新 Render、Reconciliation 和 Commit。即使 React 存在自动批处理,也不能简单认为连续到来的异步 Token 一定会被全部合并。
AI 流式输出每秒可能产生几十次甚至更多增量更新。如果每个 Token 都触发一次 React 更新,会产生大量无意义的 Render。
另外 AI Chat 通常还伴随 Markdown 解析、代码高亮、LaTeX 渲染、滚动位置更新和 DOM Layout,这些成本可能远高于单纯文本更新。
因此:
网络数据到达频率不应该直接等于 React UI 的刷新频率。
10. 怎么在 React 中解决这个问题
核心方法是将 Token 接收与 UI Render 解耦。
SSE 收到 Token 后先写入一个可变 Buffer,不立即调用 setState。
例如:
const bufferRef = useRef('');
function onToken(token: string) {
bufferRef.current += token;
}
然后定期将 Buffer 中积累的一批 Token 一次性写入 State。
Flush 可以按照固定时间间隔执行,例如每 20ms 或 50ms 更新一次,也可以结合浏览器渲染帧进行调度。
这种方式能够在不明显影响流式输出体验的前提下,大幅降低 React Render 次数。
如果 Markdown 解析成本比较高,还可以进一步将文本 State 更新和 Markdown 渲染频率分离。
11. RequestAnimationFrame 了解吗?能不能用来解决这个问题
可以。
requestAnimationFrame,简称 rAF,是浏览器提供的渲染调度 API。传入的 callback 会在浏览器准备进行下一次重绘之前执行。
因此可以把连续到来的 Token 先写入 Buffer,然后只在下一帧统一执行一次状态更新。
const bufferRef = useRef('');
const frameRef = useRef<number | null>(null);
function onToken(token: string) {
bufferRef.current += token;
if (frameRef.current === null) {
frameRef.current = requestAnimationFrame(() => {
const chunk = bufferRef.current;
bufferRef.current = '';
setContent((prev) => prev + chunk);
frameRef.current = null;
});
}
}
假设一帧之内连续收到 10 个 Token,那么这 10 个 Token 最终只会触发一次 setState。
它的优势在于 UI 更新天然与浏览器绘制节奏对齐。60Hz 屏幕下,通常最多约每 16.7ms 刷新一次。
但 rAF 并不是所有情况下都最优。
首先,后台页面中的 rAF 通常会被浏览器大幅降频甚至暂停。
其次,如果一次 Markdown 渲染本身就非常昂贵,那么每帧 60 次仍然可能太频繁,此时可以进一步采用 30ms、50ms 等时间窗口。
因此更常见的设计是:
Buffer 解决数据聚合,rAF 或 Timer 决定 UI Flush 频率。
startTransition 可以进一步降低流式更新的调度优先级,但它解决的是 React 更新优先级问题,不能替代 Buffer。
12. AI Coding 题:AI 流式问答前端会话状态管理
这道题允许使用任意 AI 工具辅助完成。
题目要求实现一个 AI 流式问答 Session Manager,处理 SSE Token 输出过程中可能出现的正常完成、主动停止、快速连续提问、组件卸载以及服务端报错。
底层流接口还刻意设计了一些异常情况,例如旧请求可能比新请求更晚返回,并且服务端调用 onError 以后仍然会继续推送 Token。
实现要点
这道题核心不是复杂算法,而是异步状态管理。
第一,需要保证只有当前请求能够更新页面。可以为每次请求维护一个独立 Request Context,通过 requestId 或对象引用判断回调是否仍属于当前请求。
例如:
function isCurrent(request) {
return (
!destroyed &&
currentRequest === request &&
request.phase === 'streaming'
);
}
当用户快速连续提问时:
request 1
request 2
request 2 成为当前请求以后,即使 request 1 的 Token、Done 或 Error 更晚到达,也直接忽略。
第二,stop()、error 和 destroy() 都需要让当前 Stream 在逻辑上立即失效。
底层网络请求是否真的被取消不是题目的关键,只要后续 callback 不再能够修改 UI 状态,就能够保证正确性。
第三,需要处理组件卸载。destroy() 之后任何异步 callback 都不能再次调用 onState。
第四,需要考虑同步重入。onState() 本身可能同步触发新的 ask()、stop() 或 destroy(),因此在调用外部 callback 后,最好重新检查当前请求是否仍然有效。
AI Coding 面试中如何展示协作过程
这类题允许使用 AI 时,重点通常不只是“最后能不能过测试”,而是面试官会观察你怎么使用 AI。
不建议拿到题以后直接把整道题复制给 AI,然后等待它生成完整答案。这样即使最终代码通过,也很难展示你自己的工程判断。
更合适的流程是先自己阅读题目,并向面试官明确说明关键状态和边界条件。
例如先指出:
正常完成
stop 后旧 token 失效
连续 ask 需要解决 stale callback
error 后底层还会继续推流
destroy 后不能再回调
然后再让 AI 辅助生成或者检查实现。
一个比较清晰的人机协作流程可以是:
面试过程中最好把自己的判断说出来。
例如可以告诉面试官:
我先不直接让 AI 写代码,这里最明显的问题是旧请求迟到以后不能污染当前状态,所以需要一个 generation 或 currentRequest 判断。Error 以后底层还会继续产生 Token,因此 error 也必须让这个 request 立即失效。确定这几个 invariant 以后,我再让 AI 按这个方案完成代码。
AI 输出以后也不要直接运行,应当先快速 Review。
重点看几个问题:
旧请求是否真的失效
stop 后是否还能进入 onToken
error 后 token 是否还会上屏
destroy 后是否可能继续 onState
连续 ask 是否存在 race condition
如果测试失败,应先分析失败原因,再决定自己修改还是继续让 AI 修复。
这样展示出来的是:
人负责理解问题、定义约束、判断方案和验证结果,AI 负责提高编码和迭代效率。
而不是单纯展示“我会把题目复制给 AI”。
AI Coding 连续追问
13. 你平常开发中,AI 生成代码的比例是多少?剩下的部分主要完成什么?为什么不继续让 AI 完成?
AI 可以承担大量代码生成,但不应该按照代码行数简单理解人与 AI 的分工。
在 AI Coding 模式下,开发者的工作逐渐从手工输入代码转向需求理解、任务拆解、技术方案、架构判断、上下文构造、Diff Review 和最终验证。
对于简单的样板代码、重复实现、局部重构,可以大量交给 AI。
对于少量冗余代码或者很小的局部修改,如果自己修改只需要几十秒,而重新描述 Prompt 和等待 AI 修改的成本更高,人工直接修改即可。
更关键的是,开发者需要判断 AI 的实现是否真正满足需求、是否破坏已有行为以及是否符合整个系统的架构。
所以 AI 可以生成绝大多数代码,但最终责任和工程判断仍然在人。
14. 如何约束 AI 符合代码风格和规范
可以通过 Rules、AGENTS.md 等项目级指令告诉 Agent 当前项目的代码结构、命名习惯、技术栈、架构边界和禁止事项。
但是自然语言 Rules 属于软约束。
能够通过程序检查的规范,最好进一步转化成工程约束,例如:
Prettier
ESLint
TypeScript
Unit Test
Integration Test
CI
对于无法自动验证的架构约束,可以进一步通过 Review Skill 或独立 Review Agent 检查。
15. 上下文过长时,AI 注意力下降,忽略规范怎么办
如果 Context 已经非常长,继续向同一个 Context 中增加更多 Rules 并不是根本解决方法。
应该尽量降低单次任务需要处理的信息量。
例如把一个大型需求拆成多个相对独立的小任务,每个任务只加载当前真正需要的代码、文档、Rules 和 Spec。
这实际上属于 Context Engineering。
同时在编码结束以后通过 Lint、Type Check、Test 或 Review 再检查一次,避免完全依赖模型在长上下文中记住所有规则。
16. Memory 和 AGENTS.md 就能够确保 AI 不遗忘吗?还有什么办法
不能保证。
Memory、Rules 或 AGENTS.md 是否始终被完整注入 Context,取决于具体 AI Coding 产品的实现。
即使这些信息始终存在于 Prompt 中,也不意味着模型一定会遵守。
LLM 对自然语言要求的执行本质上仍然存在概率性。
因此关键规范应该尽量转化成机器可执行的检查。
例如:
代码格式 → Formatter
代码规范 → ESLint
类型规则 → TypeScript
行为规则 → Test
构建正确性 → Build / CI
最终目标不是保证模型“永远不会忘记”,而是做到:
即使模型忘记或者做错,也能够被系统发现。
17. Spec 能解决这个问题吗?Spec 本身有什么问题
Spec 可以解决一部分问题。
它最大的价值是把隐式需求转化为显式约束,并减少模型的自由实现空间。
例如只告诉 AI:
实现一个消息列表。
存在很大的解释空间。
如果 Spec 明确:
消息按照 createdAt 排序。
messageId 需要去重。
流式消息与历史消息状态分离。
组件卸载以后取消请求。
模型更容易生成符合预期的实现。
但是 Spec 本身同样存在问题。
首先,Spec 也是 Context。Spec 越来越长以后,也可能增加模型的上下文负担。
其次,Spec 可能和代码发生 Drift。代码不断演进,而 Spec 没有同步更新以后,Spec 本身可能变成过时信息。
第三,Spec 依旧主要由自然语言表达,所以仍然存在理解错误和遗漏的可能。
因此 Spec 更适合定义需求和 Acceptance Criteria,而不是作为保证 AI 正确性的唯一手段。
18. 有了解过 Multi-Agent 吗?Sub-Agent 能解决这个问题吗
Sub-Agent 可以缓解一部分上下文膨胀问题。
例如一个大型任务同时包含前端、后端、数据库和测试,可以由 Orchestrator 进行任务拆分,再交给不同 Sub-Agent。
每个 Sub-Agent 只需要读取自己相关的 Context,可以降低 Context Pollution。
另外还可以利用 Context Isolation 做 Independent Review。例如 Coding Agent 完成代码以后,再启动一个新的 Review Agent,只提供 Spec 和 Git Diff,由它独立检查问题。
但 Multi-Agent 并不是 Agent 越多越好。
它同时会引入任务协调、上下文同步、接口不一致、重复工作、代码冲突以及 Token 成本等问题。
所以 Sub-Agent 更适合用于任务拆分、上下文隔离和独立 Review。
19. 还有没有更进一步的方法?
到这里后没有继续让回答。
面试官表示,再继续往后基本就已经属于工程化层面的问题了,这部分关于 AI Coding 的连续追问到这里结束。