Turbolog 引擎

帧如何通过游程编码合并为一次 console.log 调用,以及 CellBuffer、Chromium 细节和基准测试。

Turbolog 将每次 React 提交转换为一次 console.log 调用;如果没有单元格变化,则不调用。

目标

  1. 每个发生变化的帧只调用一次。
  2. 空闲时零调用。 未变化的帧在构建字符串之前就返回。
  3. 少量样式参数。 密集的 100×32 仪表盘包含 3200 个单元格,只需 149 个 %c span。

处理流水线

React ─▶ Yoga ─▶ CellBuffer ─▶ row-hash gate ─▶ RLE ─▶ console.log

CellBuffer

帧存储在用于字符、颜色和属性的扁平类型化数组中,这些数组只分配一次。

变化检测关卡

每行都会计算哈希值。如果没有变化,驱动程序会提前返回:

if (frame === null) return SKIPPED; // 0 console callsconsole.log(frame.format, ...frame.args);

帧切换

Turbolog 追加帧,仅在首帧、调用 invalidate() 之后、累积 100 帧后的空闲时以及达到 300 帧时清空控制台。DevTools 会立即移除已清空的消息,但以 50 ms 的节流间隔绘制新消息,因此每帧都清空会导致控制台闪现空白。追加的帧在一次绘制中完成切换,零宽度的幕帘 span 将上一帧挡在视野之外。向上滚动可查看更早的帧。

帧速率控制在约 15 fps。更快的帧会重置 DevTools 的节流计时并被丢弃。

游程编码

样式相同的单元格合并为一个 %c span。span 可跨越换行符,CSS 字符串则通过驻留机制复用。

console.log("%c▼ Text.js%c ready", "color:#ededed;font-weight:700", "color:#45a557");

Chromium 细节

  • 字面量 % 转义为 %%。
  • URL 和 path:line 文本会插入 word joiner,防止 DevTools 重新设置其样式。
  • span 仅使用 CSS 长属性。DevTools 会将每个展开后的属性复制到每一行。
  • span 经过调整,使背景平铺时不留间隙。

Firefox 和 Safari

Chromium 将 %c span 的每一行布局为行内块。Firefox 和 Safari 则保持 span 的行内布局,因此会忽略垂直外边距,并将长行折行。CssConsoleDriver 通过 CSS 特性检测识别浏览器,并为这两个控制台设置 14 px 行高且不留外边距,使用 text-wrap-mode: nowrap 关闭折行,再通过 line-height: 100vh 幕帘将更早的帧推到视野之外。适配控制台尺寸时会为 Firefox 的源码链接列预留空间。Safari 将 Menlo 放在字体栈首位,因为页面无法使用用户安装的 SF Mono,而 Web Inspector 可能使用该字体绘制。向 createConsoleRoot 传入 browser 可自行选择控制台。

基准测试

packages/core/test/perf.test.ts 在密集的 100×32 仪表盘上运行,取 1000 帧的平均值(Apple M5 Pro,Bun 1.4.4):

帧耗时%c span 数
全量重绘(布局、绘制、编码)0.163 ms149
单行编辑0.011 ms149
空闲0.000 ms0

状态页面会在你的浏览器中运行相同的处理流水线。在 Playground 中的演示运行期间,在 DevTools 中运行 text.stats()。

在 GitHub 上编辑此页