前端实战:如何用JavaScript模拟ChatGPT的流式响应效果

2次阅读
没有评论

共计 2683 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

ChatGPT 流式响应特点与技术难点

ChatGPT 的流式响应具有两个核心特征:实时分段传输和逐字渲染效果。技术实现上需要解决以下问题:

前端实战:如何用 JavaScript 模拟 ChatGPT 的流式响应效果

  • 网络层:需要保持长连接持续接收数据分片
  • 渲染层:需平衡性能与实时性,避免频繁 DOM 操作导致页面卡顿
  • 交互层:要处理用户中途打断、网络异常等边界情况

WebSocket vs Server-Sent Events 技术选型

WebSocket 方案

  • 优点:
  • 全双工通信,可双向交互
  • 支持二进制数据传输
  • 更低的协议开销

  • 缺点:

  • 需要额外维护连接状态
  • 服务端实现复杂度较高

SSE 方案

  • 优点:
  • 基于 HTTP 协议,兼容现有基础设施
  • 自动重连机制
  • 更简单的服务端实现

  • 缺点:

  • 仅支持服务端推送
  • 默认 UTF- 8 编码

核心实现方案

异步生成器模拟 API 响应

async function* mockChatGPTStream(prompt: string) {const words = prompt.split('').concat(['Hello','World','!']);

  for (const word of words) {
    // 模拟网络延迟
    await new Promise(resolve => setTimeout(resolve, 50));
    yield word;
  }
}

优化 DOM 更新策略

  1. 使用 DocumentFragment 批量操作 DOM
  2. 通过 requestAnimationFrame 调度渲染任务
  3. 文本节点复用避免重复创建
const renderStream = async (container: HTMLElement) => {const fragment = document.createDocumentFragment();
  const textNode = document.createTextNode('');
  fragment.appendChild(textNode);

  for await (const chunk of mockChatGPTStream('Hi')) {
    textNode.nodeValue += chunk;

    // 首次渲染才挂载 DOM
    if (!container.contains(fragment)) {requestAnimationFrame(() => {container.appendChild(fragment);
      });
    }
  }
};

完整 TypeScript 实现

class ChatStream {
  private abortController: AbortController;
  private messageQueue: string[] = [];
  private isRendering = false;

  constructor(private container: HTMLElement) {this.abortController = new AbortController();
  }

  async start(prompt: string) {
    try {const stream = this.fetchStream(prompt);

      for await (const chunk of stream) {if (this.abortController.signal.aborted) break;

        this.messageQueue.push(chunk);
        this.scheduleRender();}
    } catch (err) {console.error('Stream error:', err);
    }
  }

  private async *fetchStream(prompt: string) {
    // 实际项目中替换为真实 API 调用
    const mockResponse = ['思考中', '...', '你好', '这是', '测试'].values();

    for (const word of mockResponse) {await new Promise(r => setTimeout(r, 100));
      yield word;
    }
  }

  private scheduleRender() {if (this.isRendering) return;

    this.isRendering = true;
    requestAnimationFrame(() => {this.renderChunks();
      this.isRendering = false;
    });
  }

  private renderChunks() {const fragment = document.createDocumentFragment();

    while (this.messageQueue.length) {const textNode = document.createTextNode(this.messageQueue.shift()!);
      fragment.appendChild(textNode);
    }

    this.container.appendChild(fragment);
  }

  cancel() {this.abortController.abort();
    this.messageQueue = [];}
}

性能优化实践

大文本分块策略

  • 按句子边界分块([.!?]标点分割)
  • 动态调整分块大小(根据 FPS 监测)
  • 优先处理可视区域内容

渲染性能监测

const monitorFPS = () => {let lastTime = performance.now();
  let frameCount = 0;

  return setInterval(() => {
    frameCount++;
    const now = performance.now();

    if (now - lastTime >= 1000) {console.log(`FPS: ${frameCount}`);
      frameCount = 0;
      lastTime = now;
    }
  }, 16);
};

内存泄漏防范

  1. 及时清理事件监听器
  2. 取消未完成的异步任务
  3. 使用 WeakMap 存储 DOM 引用

生产环境避坑指南

网络中断恢复

  1. 实现断点续传标识(lastEventId)
  2. 指数退避重试策略
  3. 本地缓存未完成的消息

敏感词过滤

const filterSensitiveWords = (text: string) => {const bannedWords = ['敏感词 1', '敏感词 2'];
  return bannedWords.reduce((result, word) => result.replace(new RegExp(word, 'gi'), '***'),
    text
  );
};

移动端适配

  • 触摸事件中断处理
  • 减少 CSS 动画复杂度
  • 优化虚拟键盘交互

开放性问题

实现打字机效果速率自适应可考虑:

  1. 基于网络 RTT 动态调整速度
  2. 根据用户阅读速度学习偏好
  3. 内容复杂度分析(长句减速)
  4. 设备性能自适应(低端设备降频)
正文完
 0
评论(没有评论)