본문으로 건너뛰기

[Open WebUI] JS transition을 CSS 애니메이션으로 교체하여 스트리밍 렌더링 최적화

PR 링크: open-webui/open-webui#23257 상태: Merged | 변경: +15 / -21

들어가며

Open WebUI에서 LLM 응답을 스트리밍할 때, 각 단어가 화면에 나타나면서 fade-in 효과가 적용됩니다. 기존에는 Svelte의 transition:fade를 사용했는데, 이는 JS 기반으로 매 단어마다 메인 스레드에서 인라인 스타일을 적용하고 라이프사이클 콜백을 실행합니다. 이 PR은 동일한 시각 효과를 CSS @keyframes로 대체하여 메인 스레드 부하를 제거합니다.

핵심 코드 분석

Before: Svelte JS transition

<script lang="ts">
  import { fade } from 'svelte/transition';
  export let token;
  export let done = true;
  let texts = [];
  $: texts = (token?.raw ?? '').split(' ');
</script>

{#if done}
  {token?.raw}
{:else}
  {#each texts as text}
    <span class="" transition:fade={{ duration: 100 }}>
      {text}{' '}
    </span>
  {/each}
{/if}

transition:fade는 양방향(bidirectional)이므로, donetrue로 바뀌면 모든 단어 span이 fade-out되면서 일반 텍스트가 렌더링됩니다. 이는 의도하지 않은 시각적 결함입니다.

After: CSS @keyframes 애니메이션

<script lang="ts">
  export let token;
  export let done = true;
</script>

{#if done}
  {token?.raw}
{:else}
  {#each (token?.raw ?? '').split(' ') as text}
    <span class="fade-in-token">
      {text}{' '}
    </span>
  {/each}
{/if}
/* app.css */
@keyframes fade-in-token {
  from { opacity: 0; }
  to { opacity: 1; }
}
.fade-in-token {
  animation: fade-in-token 100ms ease-out;
}

CSS 애니메이션은 브라우저의 컴포지터 스레드에서 처리되므로 메인 스레드를 차단하지 않습니다. 또한 intro-only 애니메이션이므로 fade-out 버그도 자연스럽게 해결됩니다.

왜 이게 좋은가

1. 스트리밍 중 성능 영향

LLM이 초당 수십 개의 토큰을 생성할 때, 각 단어마다 Svelte의 JS transition이 실행되면 메인 스레드가 반복적으로 차단됩니다. CSS 애니메이션으로 전환하면 이 오버헤드가 완전히 제거됩니다.

2. 리액티브 변수 제거

$: texts 리액티브 선언을 제거하고 {#each} 블록 안에서 직접 split()을 호출합니다. 이로써 불필요한 리액티브 구독 오버헤드가 줄어들고, fade import도 제거됩니다.

3. CodespanToken 중복 제거

기존에 CodespanToken.svelte에 컴포넌트 스코프로 정의되어 있던 동일한 fade-in-token 키프레임을 전역 app.css로 이동하여 중복을 제거했습니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글