Orillusion Stats 性能监控组件详解:一行代码接入 FPS 与内存实时面板
Orillusion Stats 性能监控组件详解:一行代码接入 FPS 与内存实时面板
Orillusion 是基于 WebGPU 标准构建的纯 Web 3D 渲染引擎,而 @orillusion/stats 正是官方为它提供的性能监控插件。本文将围绕 packages/stats/README.md 展开,完整讲解该组件的安装、接入、生命周期与底层实现原理,并结合 packages/stats/index.ts 源码与仓库内的真实示例,带你彻底掌握在 Orillusion 场景中实时监控 FPS(帧率)与 JS 堆内存占用的方法,以及在浏览器全局构建(UMD/CDN)环境下的使用方式。
一、Stats 组件是什么
Stats 是 Orillusion 官方提供的一个性能监控组件(Component),直接继承自引擎的 ComponentBase 基类。它被设计为一行即可接入的场景组件:
scene.addComponent(Stats);
接入后,页面左上角会出现一个固定定位的半透明监控面板,实时显示两类关键指标:
- FPS:每秒渲染帧数,用于评估渲染性能与帧耗时稳定性;
- MB:浏览器 JS 堆内存占用(
totalJSHeapSize,单位为 MB),用于排查内存泄漏与持续增长问题。
从 packages/stats/index.ts 的源码可以看出,Stats 类的注解被标记为 @group Plugin,即它属于引擎的插件体系,与核心渲染逻辑完全解耦,可以独立安装、独立使用。
从仓库结构看,Stats 插件包(packages/stats/)只包含三个核心文件:入口实现 index.ts、构建配置 vite.config.js 与包描述 package.json,整个插件体积小、依赖少,非常适合作为理解 Orillusion 组件生命周期与插件开发的入门范本。
二、安装与版本要求
按照 packages/stats/README.md 的说明,使用 npm 安装核心引擎与 Stats 插件:
npm install @orillusion/core --save
npm install @orillusion/stats --save
其中:
@orillusion/core是引擎本体;@orillusion/stats是本篇要讲的性能监控插件。
从 packages/stats/package.json 可以看到版本约束关系:
"peerDependencies": {
"@orillusion/core": ">=0.8.0"
}
也就是说,Stats 插件要求核心引擎版本不低于 0.8.0。插件包当前版本为 0.2.6,其 main 指向 UMD 构建产物 ./dist/stats.umd.js,module 指向 ES 模块产物 ./dist/stats.es.js,types 指向 ./dist/index.d.ts,因此无论使用 ES Module、CommonJS 还是 TypeScript,都能获得对应的类型提示与构建产物。
此外,在仓库开发环境下(如直接在本仓库中跑示例),tsconfig.json 通过 paths 映射将 @orillusion/core 指向 ../../src(即引擎源码目录),将 @orillusion/* 指向 ../*,从而在 monorepo 内实现源码级联调,无需预先构建。
三、快速上手:TypeScript / ES Module 方式接入
按 packages/stats/README.md 给出的最小示例:
import { Scene3D } from "@orillusion/core"
import { Stats } from "@orillusion/stats"
let scene = new Scene3D();
scene.addComponent(Stats);
将 Stats 添加到场景节点上,组件便会自动初始化监控面板并挂载到 document.body。把它放入一个完整可运行的 Orillusion 工程中(参考仓库示例 samples/base/Sample_Transform.ts),典型代码如下:
import { Engine3D, Scene3D, AtmosphericComponent, HoverCameraController,
Object3D, MeshRenderer, BoxGeometry, LitMaterial, DirectLight,
View3D, Camera3D, GridObject } from "@orillusion/core";
import { Stats } from "@orillusion/stats";
// 初始化引擎
await Engine3D.init();
// 创建场景作为根节点
let scene3D: Scene3D = new Scene3D();
// 添加性能监控组件
scene3D.addComponent(Stats);
// 添加大气天空
let sky = scene3D.addComponent(AtmosphericComponent);
sky.sunY = 0.6;
// 创建相机
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(45, Engine3D.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(0, 0, 30);
scene3D.addChild(cameraObj);
// 创建平行光
let light: Object3D = new Object3D();
light.rotationX = 45;
light.rotationY = 30;
let dirLight: DirectLight = light.addComponent(DirectLight);
dirLight.intensity = 3;
scene3D.addChild(light);
// 创建并配置一个盒子
const box: Object3D = new Object3D();
let mr: MeshRenderer = box.addComponent(MeshRenderer);
mr.geometry = new BoxGeometry(1, 1, 1);
mr.material = new LitMaterial();
box.y = 0.5;
scene3D.addChild(box);
// 创建视图并开始渲染
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
Engine3D.startRenderView(view);
运行后,浏览器页面左上角即可看到 Stats 监控面板。这里的关键机制是:scene3D 是 Scene3D 实例,而 addComponent 由 Object3D.ts 提供——引擎会实例化传入的组件类、把实例绑定到当前对象,并调用组件内部的 __init(),随后将组件挂入 ComponentCollect 的待启动队列,等待场景开始后按生命周期自动运行。因此 scene.addComponent(Stats) 一句,就同时完成了组件的创建、初始化与注册。
四、浏览器全局构建:CDN / UMD 方式接入
不经过打包工具时,可以直接使用构建好的全局脚本(Global build)从 CDN 引入。根据 packages/stats/README.md 的说明,引入方式如下:
<script src="https://unpkg.com/@orillusion/core/dist/orillusion.umd.js"></script>
<script src="https://unpkg.com/@orillusion/stats/dist/stats.umd.js"></script>
const { Scene3D, Stats } = Orillusion
let scene = new Scene3D();
scene.addComponent(Stats);
两点使用前提值得注意:
- 脚本顺序不可颠倒:必须先引入 core 的 UMD 构建,再引入 stats 的 UMD 构建。原因是 stats 的构建配置(见 vite.config.js)将
@orillusion/core声明为external,并把全局变量名映射为Orillusion(globals: { '@orillusion/core': 'Orillusion' })。也就是说,stats 的 UMD 产物在运行时依赖全局对象Orillusion上暴露的引擎 API,必须先有 core。 - 解构来源是全局
Orillusion:由于上面提到的全局变量映射,两个包的 UMD 构建共享同一个全局命名空间,因此const { Scene3D, Stats } = Orillusion才能同时取到引擎类与 Stats 组件。
同时,packages/stats/package.json 确认了产物文件命名:stats.umd.js(浏览器全局版)与 stats.es.js(ES 模块版),与上述 CDN 引用路径一致。
五、组件生命周期:面板如何创建、隐藏与销毁
Stats 作为一个标准 Orillusion 组件,其生命周期行为完全遵循 ComponentBase 的生命周期约定。对照源码 packages/stats/index.ts,我们可以梳理出它的四个关键阶段:
1. init() —— 创建 DOM 与面板
init() {
const container = this.container = document.createElement('div')
container.className = 'stats'
container.setAttribute('style', 'display:flex;flex-direction:column;gap:1px;position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000')
this.fpsPanel = new Panel(container, 'FPS', '#0ff', '#002')
if('memory' in performance)
this.memPanel = new Panel(container, 'MB', '#f08', '#201')
this.beginTime = (performance || Date).now()
document.body.appendChild(this.container)
}
要点:
- 容器
container是公开属性(container: HTMLElement),class 默认为stats,可以借助 CSS 自定义样式; - 默认样式为固定定位在左上角(
position:fixed;top:0;left:0),纵向 flex 排列,半透明(opacity:0.9)、高 z-index(10000)保证悬浮于页面上层; - 始终创建 FPS 面板(前景色
#0ff青色、背景色#002深蓝); - 仅当浏览器支持
performance.memory(即非标准 Chromium 扩展 API)时才创建 MB 内存面板(前景色#f08、背景色#201); - 源码中还保留了一份被注释掉的
drawcallPanel(DC 面板)定义,说明该组件在设计上还预留过 draw call 统计能力,只是当前版本未启用。
2. onEnable() / onDisable() —— 显示与隐藏
onDisable() {
this.container.style.display = 'none'
}
onEnable() {
this.container.style.display = 'flex'
}
当组件的 enable 属性被切换时,ComponentBase 内部的 __start() / __stop() 会分别触发 onEnable 与 onDisable。注意这里的开关只控制显示隐藏,不会销毁面板数据,适合临时隐藏监控而不丢失统计曲线。
3. onUpdate() —— 每秒一次的采样与刷新
onUpdate() {
this.frames++
const time = this.beginTime = performance.now()
if (time >= this.prevTime + 1000) {
this.fpsPanel.update((this.frames * 1000) / (time - this.prevTime), 100)
this.memPanel?.update((performance as any).memory.totalJSHeapSize / 1048576, 256)
this.prevTime = time
this.frames = 0
}
}
采样逻辑非常清晰:
onUpdate在每一帧被引擎的组件更新管线调用(通过 ComponentBase 的_onUpdate注册到ComponentCollect的更新列表,见 ComponentCollect.ts);- 当累计时间满 1 秒(
time >= this.prevTime + 1000)时,进行一次统计刷新:FPS 按帧数 × 1000 ÷ 实际耗时计算,量程上限为 100;内存值读取performance.memory.totalJSHeapSize(字节)并除以1048576(即 1024²)换算为 MB,量程上限为 256。
也就是说,面板上的数字每秒更新一次,而不是逐帧刷新,这样既避免了频繁重绘,也保证了数值的稳定性。
4. stop() —— 销毁与清理
stop() {
this.fpsPanel.destroy()
this.memPanel?.destroy()
document.body.removeChild(this.container)
}
当组件被移除(removeComponent)或场景销毁时,stop() 会终止面板内部的 Worker、移除容器 DOM,完成资源回收。
六、面板渲染原理:OffscreenCanvas + Web Worker
这是整个插件最值得深入的部分。packages/stats/index.ts 中的 Panel 内部类没有直接在主线程绘制曲线,而是采用了 OffscreenCanvas 转交 + Web Worker 离屏渲染 的架构:
class Panel {
canvas: HTMLCanvasElement
private worker: Worker
private width = 80
private height = 48
constructor(parent: HTMLElement, name: string, fg: string, bg: string) {
const canvas = this.canvas = document.createElement('canvas')
canvas.width = this.width
canvas.height = this.height
parent.appendChild(canvas)
const offscreen = (canvas as any).transferControlToOffscreen()
const blob = new Blob([`(${worker})()`], { type: 'application/javascript' })
this.worker = new Worker(URL.createObjectURL(blob))
this.worker.postMessage({ type: 'init', offscreen, name, fg, bg }, [offscreen])
}
update(value: number, maxValue: number) {
this.worker.postMessage({ type: 'update', value, maxValue })
}
destroy() {
this.worker.terminate()
}
}
它的设计动机与收益可以总结为:
- 避免主线程开销:每个面板是一张
80 × 48的小画布。通过transferControlToOffscreen()把画布控制权转移给 OffscreenCanvas,再通过Blob内嵌的worker()函数体创建一个独立的 Web Worker; - 消息驱动:主线程只负责两件事——初始化时发送
{ type: 'init', offscreen, name, fg, bg }(offscreen 作为可转移对象被移交),以及每秒通过{ type: 'update', value, maxValue }推送采样数据; - 绘制完全在 Worker 内完成,主线程的渲染管线(尤其 WebGPU 提交、绘制调用)不会因为监控面板的 2D 绘图产生额外抖动,这也保证了 Stats 面板测出的 FPS 更接近真实渲染性能。
再看 Worker 内部的绘制函数(packages/stats/index.ts),它维护了历史数据的 min / max,在 update 消息到来时执行一次滚动曲线图绘制:
- 顶部文本区显示
当前值 + 面板名 + (历史最小值-历史最大值),例如60 FPS (45-120); - 曲线区是一个
74 × 30的滚动条带:先把已有图像整体左移 1 像素(context.drawImage(canvas, ...)自拷贝实现横向滚动),再在最右列按value / maxValue的比例绘制柱条,从而形成从左向右推进、随帧率起伏的实时波形。
这种“主线程采样 → 消息传递 → Worker 绘制”的模型,使 FPS/内存监控本身的成本降到最低,是 Web 端性能监控组件的经典实现思路。
七、自定义面板样式
由于面板容器 container 是公开字段,且 class 固定为 stats,你可以直接在页面 CSS 中覆盖样式。默认的容器样式(来自 packages/stats/index.ts)为:
/* 默认内联样式,等价描述 */
.stats {
display: flex;
flex-direction: column;
gap: 1px;
position: fixed;
top: 0;
left: 0;
cursor: pointer;
opacity: 0.9;
z-index: 10000;
}
需要调整位置(如移到右下角)、改变透明度或排列方向时,在组件 init() 完成后通过 stats.component.container.style.xxx 覆盖即可;也可以全局写 CSS 选择器 .stats { ... } 统一管理。若要彻底隐藏某个指标(如不需要内存面板),可以像源码那样在 init() 中根据 'memory' in performance 的探测结果来决定是否创建,或者自行替换为精简版组件。
八、在官方示例与基准测试中的典型用法
Stats 组件在仓库示例中被大量使用,尤其在渲染性能基准测试中几乎是标配。例如:
- samples/benchmark/Sample_drawCall.ts:在
await Engine3D.init()之后执行this.scene.addComponent(Stats),随后创建了 10 万个共享几何体的 MeshRenderer 节点,用于观察 draw call 压力下的 FPS 表现; - samples/benchmark/Sample_InstancedrawCall.ts 与 samples/benchmark/Sample_InstancedrawCall2.ts:在实例化渲染(Instanced Draw)对比场景中监控帧率;
- samples/base/Sample_Transform.ts:基础变换示例中验证面板与
dat.GUI调试面板共存; - samples/base/Sample_Destroy.ts、samples/base/Sample_MatrixAllocation.ts:配合资源创建/销毁场景,可用来观察内存面板(MB)是否随对象销毁回落,是排查内存泄漏的直观手段;
- 此外 samples/physics/、samples/gui/、samples/graphic/、samples/animation/ 等多个目录下的示例也均以
scene.addComponent(Stats)的方式接入。
这一模式验证了 Stats 组件跨模块、跨场景的通用性——无论跑物理仿真、GUI 渲染、图形特效还是大规模 draw call 基准,都能用同一行代码挂上性能监控。
九、组件构建产物一览
对于希望深入理解该插件打包方式的读者,vite.config.js 展示了它是如何被构建成库产物的:
build: {
target: 'esnext',
lib: {
entry: path.resolve('index.ts'),
name: 'Stats',
fileName: (format) => `stats.${format}.js`
},
rollupOptions: {
external: ['@orillusion/core'],
output: {
globals: { '@orillusion/core': 'Orillusion' }
}
}
}
- 构建目标为
esnext,产物分别为stats.es.js(ES 模块)与stats.umd.js(浏览器全局); @orillusion/core被标记为external,不打进产物,由宿主应用提供,且 UMD 场景下通过globals映射到全局对象Orillusion——这正是第四节 CDN 用法可行的根本原因。
总结
@orillusion/stats 以极低的接入成本(scene.addComponent(Stats) 一行代码)提供了 FPS 与 JS 堆内存两项核心指标的实时监控,并在实现上通过 OffscreenCanvas + Web Worker 把统计面板的绘制开销与 WebGPU 主渲染管线隔离。无论是日常开发调试、压测 draw call 场景,还是排查内存泄漏,它都是 Orillusion 项目里开箱即用的性能观测工具。其插件化、组件化的设计,也为开发者自行扩展更多指标(例如源码中预留的 draw call 面板)提供了清晰的参考范式。