Claude Code has a big problem — Claude Code 有個大問題

Theo 深入分析了 Claude Code 使用 React 和 Ink 進行終端渲染所引發的爭議。他解釋終端渲染與瀏覽器 DOM 的根本差異、標準緩衝區與替代模式的取捨,以及 Anthropic 團隊為何選擇 React 作為 CLI 框架。影片不只是技術解析,更揭示了一個重要的工程哲學:有時候選擇「夠好」的技術、在需要時再重寫底層,比一開始就追求最佳效能更務實。


原影片連結:https://www.youtube.com/watch?v=xrio-BsgkqU

影片重點

  • Claude Code 的終端渲染使用 React + Ink,引發社群關於效能的激烈討論
  • 終端渲染比瀏覽器 DOM 更慢,因為所有更新都是透過追加文字完成的
  • 標準緩衝區(Standard Buffer)和替代模式(Alt Mode)各有利弊,Claude Code 選擇標準緩衝區
  • React 的 Virtual DOM 模型在終端中會產生大量 ANSI 跳脫字元,導致效能瓶頸
  • Anthropic 團隊已 fork Ink 並加入原生元件,持續優化渲染管線
  • 選擇 React 的核心原因是團隊協作效率,而非極致效能
  • Boris(Claude Code 創建者)的工程哲學源自 Meta:先快速交付,再重寫底層
  • 未來趨勢可能是從終端 UI 轉向獨立桌面 UI 應用程式

詳細內容

[00:00] 事件起因:一則推文引發的風暴

Anthropic 的 Claude Code 工程師 Thariq 在 Twitter 上分享了他們如何修復 Claude Code 的畫面閃爍問題。文中提到 Claude Code 的渲染管線類似小型遊戲引擎:每一幀需要透過 React 建構場景圖(Scene Graph)、佈局元素、光柵化到 2D 畫面、與前一幀做差異比對,最後生成 ANSI 跳脫序列寫入終端。他們只有 16 毫秒的幀預算,其中 React 場景處理就佔了 11 毫秒,只剩 5 毫秒來完成 ANSI 寫入。

這段描述引發了大量批評,有人諷刺說:「我們居然活在一個用 React 驅動簡單 TUI、需要 11 毫秒來排版等寬字型文字的荒謬世界。」

[02:48] React 的渲染模型回顧

Theo 從基礎開始解釋 React 的運作方式。在 React 出現之前,開發者需要手動選取 DOM 元素並綁定行為——找到按鈕、綁定點擊事件、手動更新對應的顯示元素。React 顛覆了這個模式:將狀態放在首位,用 useState 管理資料,當狀態改變時,整個元件重新計算。

React 的核心大膽決策是:與其手動追蹤哪些元素需要更新,不如讓所有東西都重新計算,然後透過 Virtual DOM 差異比對來找出真正改變的部分,只把差異提交給瀏覽器。這個技術讓開發變得簡單許多,但前提是「提交變更」這個動作要夠快。

[05:48] 終端渲染為什麼比瀏覽器更難

有人問 Boris 為什麼不直接全部重繪而要做差異比對,Boris 回答:「終端比 DOM 慢得多。」這是一個讓很多人驚訝的事實。

終端的標準緩衝區是一個純粹的追加系統——所有輸出都是一行一行往下寫。如果你想回頭修改之前的內容,就必須使用 ANSI 跳脫字元告訴終端「跳回某個位置再修改」。這意味著每次更新都在增加需要傳送的資料量。歷史越長,跳脫序列越多,效能就越差。加上顏色、樣式等特殊字元,資料量會快速膨脹。

相比之下,瀏覽器的瓶頸在於追蹤和更新的元素數量;而終端的瓶頸在於推送的位元組數量。這是完全不同的效能特性。

[08:30] 標準緩衝區 vs 替代模式

終端有兩種主要的渲染策略:

標準緩衝區(Standard Buffer):所有輸出追加到滾動歷史中。Claude Code 使用這種模式,所以你可以選取文字、捲動查看歷史。但每次關閉 Claude Code,整個對話歷史會被寫入終端的滾動緩衝區,需要大量額外文字來覆寫舊內容。

替代模式(Alt Mode):完全接管終端畫面,不寫入滾動歷史。Open Code 使用這種模式,所以關閉後什麼都不留。但代價是必須自己實作文字選取、複製貼上等功能。

Theo 實際展示了兩者的差異:Claude Code 關閉後歷史仍在、可以正常選取文字;Open Code 關閉後畫面消失、選取行為需要自訂邏輯。這是一個核心的架構取捨。

[12:30] 視窗大小調整的挑戰

Theo 展示了不同工具處理視窗調整大小的差異。Claude Code 能正確地重新渲染所有內容,包括之前的歷史記錄,因為 React 的 Virtual DOM 會持續比對並修正差異。相對地,Codex(OpenAI 的 Rust CLI)使用標準緩衝區但不重新渲染已輸出的內容,所以當視窗縮小再放大時,之前的文字排版不會修正。

終端甚至沒有原生的「視窗大小變更」事件(不像瀏覽器有 window.onresize)。Claude Code 團隊推測是透過攔截 SIGWINCH 信號、暫時切換到替代模式來偵測視窗尺寸,然後重新計算佈局再切回來。這些都是需要手工處理的底層細節。

[16:15] 為什麼選擇 React 和 Ink

Ink 是一個將 React 帶入 CLI 世界的專案。它使用 Yoga(Meta 開發的跨平台 Flexbox 佈局引擎)來計算元素位置,然後生成對應的 ANSI 跳脫序列來渲染。這讓開發者可以用熟悉的 React 元件和 Flexbox 語法來構建 CLI 介面。

React 的最大優勢在於團隊協作:元件的隔離性讓一個開發者很難因為修改底層元件而意外破壞上層功能。這種「由上而下」的思維模式消除了整個類別的同步 bug。相比之下,其他框架如 Svelte 或 Solid 更容易因為信號綁定錯誤而導致狀態不同步。

[19:20] Boris 的 Meta 式工程哲學

Claude Code 的創建者 Boris 曾在 Facebook 工作,負責將大量程式碼遷移到 React。Theo 認為 Boris 帶來了 Meta 獨特的工程哲學:不是繞過問題或在問題之上建構,而是深入問題底層重新改造。

Meta 的經典案例包括:

  • PHP 太慢但程式碼庫太大 → 不重寫 Facebook,而是重寫 PHP(創造了 Hack 語言和 HHVM)
  • React 有效能問題 → 不換框架,而是寫 React Compiler 自動優化所有程式碼
  • JavaScript 在行動端太慢 → 打造 Static Hermes 將 JS 編譯為原生程式碼
  • 他們甚至拒絕使用 Git,改用自己 fork 的 Mercurial(後來演變成 Sapling)

Boris 對 Claude Code 的思路很可能是:React 也許不是技術上最優的選擇,但如果出了問題,可以重寫底層基礎設施來解決,而不是重寫整個應用程式。事實上,Anthropic 收購 Bun 也符合這個邏輯——這和 Meta 重寫 PHP 是完全相同的思維模式。

[23:50] Claude Code 的現狀與演進

Claude Code 最初是一個實驗性產品。如果失敗了,技術選型都不重要。但它成功了,現在是時候處理技術債務。團隊已經不再使用原始的 Ink,而是進行了大量 fork 和原生化改造。

其中一個具體問題是:渲染緩衝區中的文字量越來越大,導致垃圾回收更頻繁,每次 GC 就可能掉一幀。社群中也出現了像 Claude Chill 這樣的第三方工具,透過代理攔截來過濾掉不必要的螢幕重繪,減少發送到終端的資料量。但這種方案也有代價——捲動行為會改變、調整大小時可能出現閃爍。

[27:00] 終端 UI 的未來

Theo 坦言他越來越傾向於離開終端 UI。他最近常用 Conductor(一個 Claude Code 和 Cursor 的獨立 UI 客戶端),因為它提供了真正的圖形介面——可以點擊切換、Command+Tab 切換視窗、同時管理多個專案。

他認為終端本質上不是為 UI 設計的。即使有像 Textual 這樣令人驚嘆的 Python 終端 UI 框架能在終端中渲染複雜應用,但只要涉及視窗調整、文字選取、剪貼簿操作,問題就會浮現。這也是為什麼 Open Code 也在開發桌面應用版本。

不過 Theo 強調,Claude Code 在日常使用中仍然完全可用。偶爾的延遲確實存在,但並不影響主要工作流程。那些在 Twitter 上大聲抱怨的人,很多並不是真正的日常使用者。

我的想法

這部影片最有價值的地方不是技術細節本身,而是它揭示的工程決策思維。在技術社群中,我們常常陷入「最佳技術方案」的迷思——一定要用最快的語言、最輕量的框架、最新的範式。但 Boris 和 Meta 的哲學提醒我們:技術選型從來不只是效能比較,還要考慮團隊規模、開發速度、可維護性,以及「錯了能不能改」的彈性。

另一個值得注意的觀點是終端 UI 的天花板。隨著 AI 編碼助手越來越複雜,需要顯示差異比對、多面板、即時預覽等功能,純終端方案的限制會越來越明顯。Conductor、Open Code Desktop 這類獨立 UI 的出現很可能是趨勢,而非曇花一現。對開發者來說,理解這個轉變可能比糾結於「該用 React 還是 Ratatui」更重要。

進階測驗:Claude Code 終端渲染架構解析

測驗目標:驗證你是否能在實際情境中應用所學的終端渲染知識與工程決策思維。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個終端 AI 助手,需要決定渲染策略。你的需求是:使用者可以捲動查看對話歷史、正常選取和複製文字,但也需要在對話中動態更新 UI 元素(如進度條、狀態列)。你應該選擇哪種方案? 情境題

需求清單: 1. 使用者可以用滑鼠捲動查看過去的對話 2. 可以直接選取終端中的文字並複製 3. 需要動態更新螢幕上的狀態列和輸入框 4. 關閉程式後,對話紀錄保留在終端歷史中
  • A. 使用替代模式(Alt Mode),因為它能完整接管畫面,更容易控制 UI 元素的渲染
  • B. 使用標準緩衝區(Standard Buffer),搭配 ANSI 跳脫字元來處理動態更新
  • C. 混合使用兩種模式,平時用標準緩衝區,需要更新 UI 時切換到替代模式
  • D. 放棄終端方案,直接改用桌面 GUI 應用程式

2. 你的團隊有 30 名工程師共同開發一個複雜的終端應用程式,成員的技術背景以 Web 前端為主。產品需要快速迭代、頻繁新增 UI 元件,且必須確保一個人的修改不會影響其他人的功能。以下哪個框架選擇最合理? 情境題

團隊條件: – 30 人共同開發 – 多數人熟悉 React,少數人有 CLI 開發經驗 – 每週至少新增 2-3 個 UI 元件 – 需要元件級別的隔離性
  • A. 使用 Ratatui(Rust),因為效能最佳,且 Rust 的型別系統能保證安全性
  • B. 使用 ncurses(C),因為它是最成熟的 TUI 函式庫,穩定性最高
  • C. 使用 Ink(React),因為團隊已熟悉 React 的元件模型,且 React 的由上而下渲染能有效隔離元件間的影響
  • D. 使用 Textual(Python),因為 Python 最容易上手,開發速度最快

3. 你的終端應用在某些使用者的環境中出現嚴重的畫面閃爍問題。經過分析,你發現每次渲染都會將完整的螢幕內容(約 5000 行 ANSI 字元)作為一個原子操作寫入終端,但實際可見區域只有 20 行。根據 Claude Code 團隊的經驗,最有效的優化方向是什麼? 情境題

問題描述: – 每次更新送出約 5000 行 ANSI 跳脫序列 – 實際可見畫面只有 20 行 – 部分終端出現閃爍(frame miss) – 記憶體使用量持續上升
  • A. 將整個應用從 JavaScript 重寫為 Rust,利用 Rust 的零成本抽象來提升效能
  • B. 實作差異比對(diff),只發送實際變更的部分到終端,減少推送的位元組數量
  • C. 增加幀預算到 32 毫秒(降低到 30 fps),讓每幀有更多時間完成渲染
  • D. 關閉所有顏色和樣式渲染,減少 ANSI 跳脫字元的數量

4. 一位開發者用 Ink(React CLI 框架)構建了一個終端應用程式。他發現視窗調整大小後,之前渲染的內容不會重新排版,只有新輸出的內容才能正確適應新的視窗寬度。他認為這是 React 的 Virtual DOM 差異比對失敗了。這個診斷正確嗎? 錯誤診斷

症狀: 1. 視窗從 120 列縮小到 80 列 2. 之前輸出的長行文字沒有重新換行 3. 新輸出的內容正確顯示在 80 列寬度 4. 開發者結論:「React 的 diff 演算法有 bug」
  • A. 診斷正確,React 的 Virtual DOM 在終端環境中的差異比對確實不可靠
  • B. 問題出在 Yoga 佈局引擎沒有正確回應視窗大小變更事件
  • C. 問題不在 React,而在終端的標準緩衝區特性——已經寫入滾動歷史的內容是純文字,終端本身不會重新排版它們
  • D. 問題出在作業系統的終端模擬器沒有發送 SIGWINCH 信號

5. 一個團隊開發了標準緩衝區的終端應用,運行一段時間後出現嚴重的效能衰退。記憶體持續增長,渲染偶爾會卡頓掉幀。他們最初的判斷是「JavaScript 引擎的垃圾回收太頻繁」。以下哪個才是更準確的根本原因分析? 錯誤診斷

觀察到的現象: – 應用剛啟動時流暢(60fps) – 使用 30 分鐘後開始出現間歇性卡頓 – 記憶體從 50MB 增長到 300MB+ – 卡頓發生時,可觀察到 GC 暫停事件 – 每次更新發送的 ANSI 位元組數持續增長
  • A. JavaScript V8 引擎的垃圾回收器設計有缺陷,無法處理大量字串操作
  • B. 隨著終端歷史增長,需要更多 ANSI 跳脫字元來定位和更新舊位置,導致字串緩衝區持續膨脹,最終觸發更頻繁的垃圾回收
  • C. React 的 Virtual DOM 樹隨著元件增多而變大,導致每次差異比對需要更多時間
  • D. 終端模擬器本身的渲染效能隨著滾動緩衝區增大而線性衰退
0

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *