diff --git a/src/Example/Program.cs b/src/Example/Program.cs
index ac8e5f4..63465d3 100644
--- a/src/Example/Program.cs
+++ b/src/Example/Program.cs
@@ -11,10 +11,10 @@ using var terminalInput = new ConsoleTerminalInput();
var terminalOutput = new ConsoleTerminalOutput();
var parser = new DefaultInputParser();
var textMeasurer = new TerminalTextMeasurer();
-var renderer = new FullScreenRenderer(terminalOutput, textMeasurer);
+var renderer = new DifferentialRenderer(terminalOutput, textMeasurer);
using var runtime = new TuiRuntime(terminalInput, parser, renderer);
-var component = new EchoComponent(runtime, textMeasurer);
+var component = new EchoComponent(runtime, textMeasurer, renderer);
runtime.Add(component);
runtime.SetFocus(component);
@@ -36,7 +36,7 @@ finally
///
/// 用于手动验证 Runtime 的示例组件
///
-file sealed class EchoComponent(ITuiRuntime runtime, ITextMeasurer textMeasurer) : IInputComponent
+file sealed class EchoComponent(ITuiRuntime runtime, ITextMeasurer textMeasurer, DifferentialRenderer renderer) : IInputComponent
{
private readonly ManualResetEventSlim _done = new();
private readonly List _events = [];
@@ -49,6 +49,8 @@ file sealed class EchoComponent(ITuiRuntime runtime, ITextMeasurer textMeasurer)
"TinyTUI Runtime 示例",
"====================",
$"当前渲染宽度: {width}",
+ $"全量重绘次数: {renderer.FullRedrawCount}",
+ $"差分重绘次数: {renderer.DifferentialRedrawCount}",
string.Empty,
"测试方式:",
"- 输入普通字符 验证 Text 事件会进入焦点组件",
diff --git a/src/TinyTUI/Rendering/DifferentialRenderer.cs b/src/TinyTUI/Rendering/DifferentialRenderer.cs
new file mode 100644
index 0000000..3ab1c7e
--- /dev/null
+++ b/src/TinyTUI/Rendering/DifferentialRenderer.cs
@@ -0,0 +1,103 @@
+using TinyTUI.Stdout;
+using TinyTUI.Text;
+
+namespace TinyTUI.Rendering;
+
+///
+/// 只重绘变化行的基础差分渲染器
+///
+public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer? textMeasurer = null) : IRenderer
+{
+ private readonly ITextMeasurer _textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
+ private List _previousLines = [];
+ private TerminalSize? _previousSize;
+
+ ///
+ /// 获取累计全量重绘次数
+ ///
+ public int FullRedrawCount { get; private set; }
+
+ ///
+ /// 获取累计差分重绘次数
+ ///
+ public int DifferentialRedrawCount { get; private set; }
+
+ ///
+ public void Render(IReadOnlyList lines, TerminalSize size)
+ {
+ var nextLines = NormalizeLines(lines, size.Columns);
+
+ if (_previousSize != size)
+ {
+ RenderFull(nextLines, size);
+ return;
+ }
+
+ RenderDiff(nextLines, size);
+ }
+
+ ///
+ public void Reset()
+ {
+ _previousLines = [];
+ _previousSize = null;
+ output.ClearScreen();
+ output.Flush();
+ }
+
+ private void RenderFull(List lines, TerminalSize size)
+ {
+ FullRedrawCount++;
+ output.ClearScreen();
+
+ foreach (var line in lines)
+ {
+ output.Write(line);
+ output.Write("\r\n");
+ }
+
+ output.Flush();
+ _previousLines = lines;
+ _previousSize = size;
+ }
+
+ private void RenderDiff(List lines, TerminalSize size)
+ {
+ var maxLineCount = Math.Max(lines.Count, _previousLines.Count);
+ var changed = false;
+
+ for (var index = 0; index < maxLineCount; index++)
+ {
+ var previous = index < _previousLines.Count ? _previousLines[index] : string.Empty;
+ var next = index < lines.Count ? lines[index] : string.Empty;
+
+ if (previous == next)
+ {
+ continue;
+ }
+
+ changed = true;
+ output.Write($"\e[{index + 1};1H");
+ output.Write("\e[2K");
+
+ if (next.Length > 0)
+ {
+ output.Write(next);
+ }
+ }
+
+ if (changed)
+ {
+ DifferentialRedrawCount++;
+ output.Flush();
+ }
+
+ _previousLines = lines;
+ _previousSize = size;
+ }
+
+ private List NormalizeLines(IReadOnlyList lines, int width)
+ {
+ return [.. lines.Select(line => _textMeasurer.Truncate(line, width))];
+ }
+}