字节产研 dev&infra 一面:JSON to HTML、TTU 与限流调度手撕
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 解释选择 JSON to HTML 而非直接生成 HTML 的原因。
- LCP 和首屏包体积都明显下降——先说清两个数字怎么测的,再选一个改动,从网络加载、JS 执行到浏览器绘制解释为什么有效。
- 你们的 TTU 怎么定义?
- 完整处理链路各阶段的入参出参、作用,以及 schema 校验后仍需校验的原因。
- 场景题一:用户说「看看这个作业为什么慢」,只给了一个作业名,LLM 识别置信度很高,但它可能对应多个集群、也可能没有权限。请设计路由与执行流程;如果规则和模型判断冲突,谁说了算?
- 场景题二:有 Gateway、Worker、Web UI,Worker 执行诊断,Gateway 提供任务查询和事件订阅,UI 展示过程与最终报告。要支持报告 v2、断线重连、取消、新旧 Worker 并存,画出最小数据流并说明报告与事件字段;报告已生成但 UI 断线、两个子步骤一成一败时,任务状态由谁决定?前端如何避免重连后重复内容或状态倒退?
- 手撕:实现
async function runLimited(tasks, limit)。
《参考解析》
JSON to HTML 而不是直接生成 HTML:核心是信任边界。让模型直接产出 HTML 等于把可执行的标记交给模型,XSS、内联脚本、越权资源引用都不可控;只让它产出结构化 JSON,再由受控渲染器映射成组件树,所有标签与属性都来自白名单,样式与脚本根本不进入产出。附带收益是可校验、可缓存、可局部更新——JSON 能做 schema 校验、能 diff 出变化的部分只重渲染对应节点,HTML 只能整块替换。代价是表达能力受限、需要写渲染器,以及模型要理解你的 schema。
LCP 与包体积的测量与归因:LCP 用 PerformanceObserver 监听 largest-contentful-paint,取页面首次交互前的最后一次候选(注意 LCP 会被后续加载的元素更新,且 SPA 路由切换后要重置);包体积看构建产物(gzip/brotli 后大小)+ 依赖分析(哪个包占大头、是否被重复打入)。归因要把「首屏要加载的内容」拆成三段:HTML 与关键 CSS(决定首帧)、JS 下载与解析执行(决定能否渲染)、字体与图片(决定最大元素何时画完)。改「JSON to HTML」之所以有效,通常是减少了客户端要执行的渲染逻辑与要下载的数据量——数据变小了、解析路径变短了、不再需要等某个大 chunk 执行完才敢渲染。
TTU 的定义:TTU(Time To Usable)是「用户能真正开始操作」的时间,比 FCP/LCP 更靠后。可以定义为「首屏关键交互可响应」的时刻,度量的方式有两种:一是自定义打点——在关键组件 hydration 完成、事件handler 绑定完毕时上报;二是用 Long Task 与 INP 的观测间接推断。定义要说清边界:包包不包括空闲回调、要不要等非关键模块懒加载完成。工程上它比 LCP 更接近体感,因为它衡量的正是「点了有没有反应」。
schema 校验后为什么还要校验:因为层与层之间的信任不同。schema 校验只能保证结构合规,管不了语义:字段是合法字符串但值是错的、是合法的数字但越界、是合法的枚举但当前状态不允许该操作、是合法对象但引用了不属于该租户的资源。另外上游可能被绕过(内部调用、重放、旧版本客户端),所以每层边界都要按「不可信输入」处理,且业务规则校验必须在服务端做最终裁定。
LLM 路由与规则冲突:流程设计成「解析 → 消歧 → 鉴权 → 执行」。意图识别给出的是候选集合 + 置信度,而不是唯一答案;置信度高但存在多义(多个集群同名)时,用确定性规则做消歧(用户所属组织、最近访问、权限范围),消歧不唯一就返回候选让用户选。冲突时的原则是:安全与权限类规则永远压过模型判断(没有权限就是没有,不管模型多确信),业务偏好类可以让模型优先但必须可撤销、可审计。所有决策打点,便于事后核对「模型判断导致了多少次误操作」。
报告 v2、断线重连与状态归属:数据流是 Worker → 事件/报告写入存储 → Gateway 提供查询与订阅 → UI 订阅增量事件 + 拉取快照。字段设计上,报告要有 version(v1/v2 同存时按版本解析)、revision(单调递增)、status(pending/running/partial/succeeded/failed/cancelled)和分段结果数组(每段有自己状态)。断线重连的正确姿势是先拉快照拿到 revision,再订阅该 revision 之后的事件,合并时按 revision 丢弃旧事件,UI 就不会重复或倒退。任务终态由服务端决定而不是前端推断:两个子步骤一成一败时,整体状态是 partial(或按业务定义的严格模式 failed),这条规则必须写在一个地方(服务端的聚合逻辑),前端只展示。
手撕 runLimited:要点是「启动时立即补位」。维护一个游标 next 和一个结果数组;核心是一个 worker 循环:取下一个索引 → 若越界则返回 → 调用任务函数(用 Promise.resolve().then() 包住以捕获同步抛错)→ 写结果 → 循环。启动 min(limit, n) 个 worker 并 Promise.all。参数校验:!Number.isInteger(limit) || limit <= 0 时返回 Promise.reject(new RangeError(...)),且要在空数组之前校验。注意结果按索引写入保证顺序,错误对象原样保存为 reason。