核心是解耦流式数据接收与渲染,用requestAnimationFrame按帧率逐段渲染;缓存文本、渐进解析Markdown、安全插入HTML;支持中断重连;避免高频DOM操作,优化性能。

核心在于把流式数据的接收、拼接与视觉呈现解耦,用浏览器原生帧率控制逐字渲染节奏,而不是简单地追加 innerHTML。
用 requestAnimationFrame 控制渲染节奏
直接在 onmessage 或 data 事件里频繁修改 DOM(比如反复 += 字符)会导致布局抖动、卡顿甚至丢帧。正确做法是把收到的文本片段缓存起来,再用 requestAnimationFrame 按固定帧率(约每 16ms 一帧)取一个字符或小段文本进行渲染。
- 每次收到新 chunk,先追加到一个字符串缓冲区(如
pendingText),不立即更新 DOM - 若当前没有活跃的动画帧任务,调用
requestAnimationFrame(renderNextChar) -
renderNextChar函数从缓冲区取出前 1–3 个字符(避免单字太慢、整段太快),插入到目标元素末尾,并更新光标位置 - 若缓冲区还有剩余,继续调度下一帧;清空后停止
支持 Markdown 的渐进式解析与安全插入
不能等全部文本收完再用 marked 全量解析——那样就失去流式意义。要实现“边来边转”,关键是在每次渲染前只解析**已确定不会被后续内容截断的最小语义单元**。
将小说章节转换为电影分镜剧本。用户上传txt/md/docx文本,AI分析场景、角色、情绪、镜头语言,输出专业分镜脚本。适用于用户提及“分镜”“storyboard”“小说转分镜”“影视改编”“镜头脚本”或需要将小说改编为分镜的场景。
- 对当前待渲染的文本段做轻量预处理:识别是否以完整 Markdown 行结尾(如以
\n、```、1.结尾),避免把未闭合的代码块或列表提前渲染成错误结构 - 使用
marked.parseInline()处理短文本(如加粗、链接),用marked.parse()处理含换行的块级内容,但需加白名单过滤(禁用 script、iframe 标签) - 渲染时用
element.insertAdjacentHTML('beforeend', safeHtml),而非innerHTML = ...,避免重绘整个容器
处理中断、重连与边界情况
真实场景中,流可能被网络中断、服务端超时或用户主动取消。打字机效果必须能优雅应对这些状态变化。
- 用 AbortController 绑定 fetch/SSE 请求,cancel 后清空 pendingText 并停止动画帧
- 当收到
data: [DONE]或空消息时,确认流结束,触发最终渲染和滚动到底部 - 若某次 chunk 解析失败(如 JSON 格式错误),跳过该条,记录日志,继续处理下一条,不阻塞整体流程
- 光标动画单独用 CSS 实现(
border-right: 2px solid #000; animation: blink 1s infinite;),与文本渲染解耦,避免闪烁不同步
避免常见性能陷阱
高频 DOM 操作是最大隐患。平滑不等于“多刷”,而在于“稳刷”。
- 不要为每个字符创建新 TextNode;批量插入(如每次 2–5 字符)更高效
- 禁用自动滚动的
scrollTop = scrollHeight在每一帧都执行;改为只在完成一段逻辑块(如一个段落或列表项)后触发一次 - 对长响应启用虚拟滚动:只渲染可视区域 + 缓冲区内的文本节点,超出部分用占位高度替代
- 使用
document.createDocumentFragment()批量构建节点,最后一次性 append 到 DOM

















