LLM 채팅 UI에서 240 FPS를 추구하기

발행: (2025년 12월 16일 오전 02:52 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

TL;DR

React UI에서 스트리밍 LLM 응답을 테스트하기 위한 벤치마크 스위트를 만들었습니다. 주요 요점:

  1. 먼저 올바른 상태를 구축하고, 나중에 렌더링을 최적화하세요. 가능하면 상태를 React 외부(예: Zustand)에 두고 필요에 따라 연결합니다.
  2. React 재렌더링이나 훅에 집중하지 말고 윈도잉에 우선순위를 두세요. memo, useTransition, useDeferredValue 등으로 얻는 이득은 윈도잉 기법에 비해 미미합니다.
  3. Critical Rendering Path(CRP)를 CSS로 최적화하세요. content-visibility: autocontain: content 같은 속성을 사용합니다. 애니메이션을 쓰는 경우 will-change를 추가하세요.
  4. 가능한 경우 LLM이 원시 마크다운을 반환하지 않도록 하세요. 마크다운을 파싱하고 렌더링하는 비용이 큽니다. 반드시 필요하다면 응답을 일반 텍스트와 마크다운 구간으로 나누고 마크다운 부분만 파싱합니다.
  5. 네트워크 속도가 병목이 아니라면 스트림을 스로틀링하세요. 단어 사이에 5–10 ms 정도의 짧은 지연을 두면 FPS를 더 높게 유지할 수 있습니다.

How LLM Streaming Works

LLM은 전체 답변을 한 번에 보내는 것이 아니라 Server‑Sent Events(SSE)를 통해 청크 단위로 스트리밍합니다.

// Basic LLM stream handling with fetch
async function streamLLMResponse(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let done = false;

  while (!done) {
    const { value, done: streamDone } = await reader.read();
    if (streamDone) {
      done = true;
      break;
    }
    const chunk = new TextDecoder().decode(value);
    // Process the chunk (can be one or multiple words)
    console.log(chunk);
  }
}

Handling the Chunk in React – Why It Lags

가장 단순한 접근법은 들어오는 청크마다 React 상태를 업데이트하는 것입니다.

import { useState } from "react";

const [response, setResponse] = useState("");

async function streamLLMResponse(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let done = false;

  while (!done) {
    const { value, done: streamDone } = await reader.read();
    if (streamDone) {
      done = true;
      break;
    }
    const chunk = new TextDecoder().decode(value);
    setResponse(prev => prev + chunk); // Update state with each chunk
  }
}

채팅 기록이 커질수록 각 상태 업데이트가 전체 컴포넌트 트리를 다시 렌더링하게 되고, FPS가 급격히 떨어집니다(몇 초 안에 0 FPS까지 떨어지는 경우가 많음).

Laggy React state updates

Optimizations

벤치마크 스위트에는 다음이 포함됩니다:

  • 청크를 일정 지연과 함께 스트리밍하는 최소 Node + TypeScript 서버(LLM 스트림을 시뮬레이션).
  • 다양한 최적화를 구현한 React + Vite + TypeScript 프론트엔드.
  • 스트리밍 중 FPS로 측정한 성능; 최적화마다 RAM 사용량이 달라집니다.

RAF Batching

들어오는 청크를 버퍼링하고 requestAnimationFrame(RAF)당 한 번씩 상태에 플러시합니다.

import { useRef, useState } from "react";

const [response, setResponse] = useState("");
const bufferRef = useRef("");

async function streamLLMResponse(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    bufferRef.current += new TextDecoder().decode(value); // Collect chunk

    requestAnimationFrame(() => {
      if (bufferRef.current) {
        setResponse(prev => prev + bufferRef.current); // Flush once per frame
        bufferRef.current = "";
      }
    });
  }
}

Result: Min FPS ≈ 15 after 90 seconds—noticeable improvement but still not ideal.

RAF batching performance

React 18 startTransition

startTransition은 업데이트를 비긴급으로 표시해 React가 긴급 UI 작업(예: 사용자 입력)을 스트리밍 텍스트 업데이트보다 우선 처리하도록 합니다.

import { startTransition, useState } from "react";

function handleInputChange(e) {
  const value = e.target.value;
  setInputValue(value); // Urgent

  startTransition(() => {
    setFilteredList(filterList(value)); // Non‑urgent
  });
}

Caveat: 청크가 React가 양보할 수 있는 속도보다 빨리 들어오면 메인 스레드가 여전히 포화될 수 있어 startTransition의 이점이 제한됩니다.

Additional Tips

  • Windowing / Virtualization: 채팅 기록 중 화면에 보이는 부분만 렌더링하세요(예: react-window 또는 react-virtualized 사용). 이렇게 하면 DOM 크기와 재페인팅 비용이 크게 감소합니다.

  • CSS Optimizations:

    .chat-message {
      content-visibility: auto;
      contain: content;
    }

    애니메이션 요소에는 will-change를 사용해 브라우저에 곧 일어날 변화를 알려줍니다.

  • Chunk Throttling: 청크 처리 사이에 인위적인 짧은 지연(5–10 ms)을 두면 렌더링 스파이크를 부드럽게 만들 수 있으며, 인지된 반응성에는 거의 영향을 주지 않습니다.

  • State Management Outside React: Zustand 같은 라이브러리를 사용하면 변형 가능한 스토어를 유지하고 React가 덜 자주 읽게 할 수 있어, 상태 업데이트에 따른 재렌더링 횟수를 줄일 수 있습니다.

RAF 배칭, 윈도잉, CRP‑중심 CSS, 그리고 신중한 상태 관리를 결합하면 안정적인 60 FPS 경험에 접근할 수 있으며, 성능이 좋은 하드웨어에서는 LLM 채팅 UI에서 240 FPS까지도 끌어올릴 수 있습니다.

Back to Blog

관련 글

더 보기 »