15 KiB
机器人手臂3D查看器 · 性能优化方案
目标文件:G:\Robot_v2\机器人手臂\机器人手臂3D查看器.html(15.31 MB / 35986 行)
用户要求:先做完整备份(带版本标识)→ 优化 → 验证功能完好 → 记录优化项与性能数据。
一、现状分析(实测,非猜测)
1.1 文件结构
| 位置 | 内容 |
|---|---|
| L1~L420 | HTML + CSS + 界面骨架(左栏/IK 控制条/舞台/页脚/BOM 抽屉) |
| L421~L428 | <script> three.js r159 内联(652 KB,UMD build,无 CDN) |
| L429~L33251 | <script>:DATA(13.99 MB 网格 JSON)+ SHEETS(52 KB SVG)+ TRAVEL + COLLIDE + 查看器主逻辑(const TL = {} 起 738 行) |
| L33252~L35022 | <script>:arm-ik-solver.js 逆运动学求解器(1770 行,自带 window.ArmIK) |
| L35023~L35984 | <script>:ik-glue.js 界面胶水层(961 行,Worker 化求解、预置自检、姿态拖拽) |
四个内联脚本的索引被 chkSig() 指纹逻辑依赖(document.scripts[2] = 求解器源码),因此本次优化不得增删 <script> 标签、不得改动求解器脚本块,否则预置自检指纹失效(会误报"预置结果已失效")。
1.2 数据规模(node 实测)
- 零件 92 件(打印件 9 + 电机 6 + 螺钉 72 + 包络 1 + 经纬线 1)
- 顶点 354,697 / 三角面 340,178 / 线段 10,296
- 单件最大顶点 47,483(< 65536 → 索引可全部用 Uint16)
JSON.parse69 ms;按查看器原样建几何(含computeVertexNormals/ 包围盒球)37 ms- 全场景射线拾取(92 件、34 万面):1.3 ms/次(60 次均值),200 次连测 2.1 ms/次
scene.updateMatrixWorld(true)全量强制刷新:0.004 ms/次(≈110 个对象)
1.3 已定位的性能问题
| # | 问题 | 证据 | 影响 |
|---|---|---|---|
| P1 | rAF 循环无条件每帧 resize() + updCam() + rend.render();needRender 被 requestRender() 置位后全文件没有任何读取点(Grep needRender 仅 3 处:声明、置位、注释) |
L33207~L33215 | 空闲时仍 60 fps 满帧渲染 34 万面 ×92 draw call;preserveDrawingBuffer:true 使每帧多一次缓冲拷贝;持续占用 GPU/功耗 |
| P2 | resize() 每帧读 stage.clientWidth/clientHeight |
同上 | 每帧强制布局(forced reflow),与 P1 同源 |
| P3 | pick() 每次 pointermove 先 meshes.filter(m => m.visible) 再全量射线求交 |
L32668 |
每帧≥1 次 1.3~2.1 ms 求交 + 每帧一个 92 元素临时数组,鼠标扫过模型时持续吃 CPU |
| P4 | applyJoint() 每次新建 2 个 Vector3 + 3 个 Matrix4,并线性查 JO.find |
L32506~L32518 | 每写一个关节角 = 6 次调用 = 30 个临时对象;动画/实时追踪每帧一批 |
| P5 | 几何索引一律 Uint32Array |
L32531、L32546 | 索引显存 340,178×3×4 B = 4.08 MB(Uint16 只需 2.04 MB) |
| P6 | 点选/ESC 时 for (const tr of tb.children) tr.style.background = '' 全表刷 style |
L32727、L32736、L32745 | 每次点击写 ~92 行样式 |
| P7 | Worker 的 Blob URL 不回收(URL.createObjectURL 后无 revokeObjectURL) |
L35510 附近 | 常驻一份 ~86 KB Blob;构造失败路径同样泄漏 |
1.4 经实测否决的优化(不做,写进报告避免后人重复劳动)
- 全量矩阵刷新:0.004 ms/次 → 改"6 级关节链更新"只有 13× 相对提速但绝对量 4 μs,风险不值。
updXform()(92 件)/updStat()(92 件 reduce+filter):量级为微秒,爆炸滑块拖动时也远低于 1 ms。- 材质合并 / 几何合并:只有 92 个 draw call,合并会破坏"零件树勾选显隐 + 点选 + 单独高亮"这些核心功能。
- 分片/延迟建场景:初始化总计约 106 ms(69 解析 + 37 建几何),分片只会增加复杂度,不降低总时间。
- 实时追踪时的结果面板 DOM 节流:
renderResult()含"场景图残差复算"与"备选构型"两块验证语义内容(用户此前专门建立了指纹/预置自检体系),节流会削弱验证可观测性,保持原样。
1.5 关键发现:源文件与成品已漂移(影响改动范围)
src/build_viewer.py 由 src/viewer_tpl.html + web/ik-glue.js + web/arm-ik-solver.js + web/mesh_arm.json 拼装成品,但:
- 成品 L35023~L35984 的 glue 区域(961 行)比
web/ik-glue.js(776 行)新:Worker 化求解、PRECOMPUTED_CHK预置自检、refreshJointLabelsLazy节流、±360°滑块等只存在于成品内。 - 成品主逻辑区与
src/viewer_tpl.html的差异只有:①±360°滑块范围(成品新)②/*__SCREWS__*/占位符展开(预期)。
已与用户确认的处理方式:本次直接改成品 HTML;同时把"能对得上"的性能补丁同步回 src/viewer_tpl.html(源文件落后部分保持原状)。在源文件完成反向同步之前不要重跑 build_viewer.py(会把成品回退到旧功能)——此警告写入报告。
二、改动清单
改动 0:备份(先做,且必须先验证备份完整)
- 新建目录
backups/,复制原件为backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html(v1 = 优化前版本号;2151 取自原件最后写入时间 2026-09-29 21:51) - 记录
SHA-256与字节数(Get-FileHash),校验备份与原件一致后再动原件。 .gitignore追加backups/,避免 15 MB 备份污染 git 状态。
改动 1(P1+P2,核心):按需渲染
文件:成品 loop()(L33207~L33215)与 src/viewer_tpl.html 对应段。
/* 改动前 */
function loop() {
resize();
if (cv.style.display !== 'none') { updCam(); rend.render(scene, cam); }
requestAnimationFrame(loop);
}
/* 改动后 */
/* 按需渲染:needRender 由 requestRender() 置位(所有交互/状态变化都已调用它);
旧版置位后无人读取,故恒以 60 fps 空转。装配图页(画布隐藏)不清标志,
切回三维页由 tab 处理器 resize()+requestRender() 补渲染。 */
function loop() {
if (needRender && cv.style.display !== 'none') {
needRender = false;
resize();
updCam();
rend.render(scene, cam);
}
requestAnimationFrame(loop);
}
要点:
- 保留
preserveDrawingBuffer:true:按需渲染后画布不再重绘,必须靠它保住最后一帧图像。 - 保留 rAF 循环:
animTo()动画、实时追踪、拖拽都依赖后续 rAF 里的渲染。 - 已核对全部渲染状态变更点均有
requestRender()(主逻辑 36 处;glue 的refreshTarget()/setJoints()/highlightSpec()/setView()内部均调用),无遗漏。 resize()仍在渲染前调用一次(进入渲染路径时同步画布尺寸),窗口尺寸变化由ResizeObserver → resize() → requestRender()覆盖。
改动 2(P3):拾取零分配 + 悬浮限流
文件:成品 L32642~L32682 与 src/viewer_tpl.html 对应段。
pick():meshes.filter(...)→ 复用模块级数组(three r159 的 Raycaster 不检查const pickList = [], ndcPick = new THREE.Vector2(); function pick(e) { const r = cv.getBoundingClientRect(); ndcPick.set(((e.clientX-r.left)/r.width)*2-1, -((e.clientY-r.top)/r.height)*2+1); ray.setFromCamera(ndcPick, cam); pickList.length = 0; for (const m of meshes) if (m.visible) pickList.push(m); // 语义与 filter 完全一致 const hit = ray.intersectObjects(pickList, false)[0]; return hit ? hit.object : null; }object.visible,已在源码中确认,故可见性过滤必须保留。)schedHover():rAF 节流 → 60 ms 时间配额 + 位置去重(实测 1.3 ms/次求交,60 次/s → ~16 次/s):尾部仍会补一次(指针停住后取最新坐标),tooltip 行为不变;点击/右键拾取仍立即执行,不受限流影响。let hoverPending = false, lastHoverEv = null, hoverT = -1e9, hoverX = -1, hoverY = -1; const HOVER_MS = 60; function schedHover(e) { if (e.clientX === hoverX && e.clientY === hoverY) return; // 未移动不重算 hoverX = e.clientX; hoverY = e.clientY; lastHoverEv = e; if (hoverPending) return; hoverPending = true; setTimeout(() => { hoverPending = false; if (!lastHoverEv || drag) return; hoverT = performance.now(); hover(lastHoverEv); }, Math.max(0, HOVER_MS - (performance.now() - hoverT))); }
改动 3(P4):applyJoint() 去分配
const JP = {}, JD = {};
for (const j of JO) { JP[j.id] = new V3(...VC(j.rear)); JD[j.id] = new V3(...VC(j.d)).normalize(); }
const _mT = new THREE.Matrix4(), _mR = new THREE.Matrix4(), _mTi = new THREE.Matrix4();
function applyJoint(id) {
const P = JP[id], D = JD[id]; if (!P) return;
const a = (ang[id] || 0) * Math.PI / 180;
JG[id].matrix.copy(_mT.makeTranslation(P.x, P.y, P.z)
.multiply(_mR.makeRotationAxis(D, a))
.multiply(_mTi.makeTranslation(-P.x, -P.y, -P.z)));
JG[id].matrixWorldNeedsUpdate = true;
requestRender();
}
数学与原式逐位相同(T(P)·R(D,θ)·T(−P)),仅把每次新建的 5 个对象换成预计算 + 3 个共享矩阵。
改动 4(P5):索引降位(Uint16)
const sIdx = p.v.length > 65535 ? new Uint32Array(p.s) : new Uint16Array(p.s); // line 分支
const idx = p.v.length > 65535 ? new Uint32Array(p.t.length*3) : new Uint16Array(p.t.length*3);
全部 92 件顶点数 ≤ 47,483(实测),Uint16 合法;索引内存 4.08 MB → 2.04 MB,上传带宽减半。保留 >65535 分支作安全兜底。
改动 5(P6):BOM 高亮行只记一行
新增 let bomHlRow = null; function bomHlClear() { if (bomHlRow) { bomHlRow.style.background = ''; bomHlRow = null; } },替换 L32727、L32736、L32745 三处全表 tr.style.background = '' 循环;设置高亮处同步记录 bomHlRow = tr。可见结果完全一致。
改动 6(P7):Worker Blob URL 回收
const u = URL.createObjectURL(new Blob([src + boot], { type: 'application/javascript' }));
let w; try { w = new Worker(u); } finally { URL.revokeObjectURL(u); }
(URL 只在 Worker 构造时被读取,构造后即可回收;失败路径也不再泄漏。此改动仅成品有——web/ik-glue.js 仍是旧同步求解版,无 Worker。)
改动 7:源文件同步(用户已确认)
src/viewer_tpl.html:逐条应用改动 1~5(该文件对应代码段存在,锚点为唯一代码串;±360°差异行不与改动区重叠)。web/ik-glue.js:核对后逐条应用(预计仅"渲染相关"部分不适用——拾取/循环/几何都在模板里;Worker 相关不存在)。- 同步后不重跑
build_viewer.py(会回退旧功能),源文件只作为"补丁已留存"。
改动 8:文档
新建 docs/查看器性能优化报告.md(与 docs/姿态估计算法评估报告.md 同目录同风格,中文):优化项逐条说明(问题/证据/改法/收益)、前后实测数据表、功能验证清单与结论、已否决项、备份路径 + SHA-256、**"源文件漂移未修,勿重跑构建"**警告。
三、假定与决策
- 只改代码区,不动
DATA/SHEETS/TRAVEL/COLLIDE数据行与求解器脚本(指纹依赖)。 - 不新增/不拆分
<script>标签(document.scripts[2]索引依赖)。 - 不改
antialias/setPixelRatio(min(dpr,1.5))/ 灯光 / 材质参数——属画质取舍,且实测 draw call 与面数不构成瓶颈。 - 测量用的"插桩副本"只放在
scratch/perf/(已被.gitignore忽略),成品文件保持干净。 - 备份放项目内
backups/(用户已选),并写入.gitignore。
四、验证方案
4.1 静态校验
backups/内备份与原件SHA-256一致(改前)。- 抽取改后文件的 4 个
<script>块到scratch/perf/,逐个node --check通过。 - 求解器脚本块 +
TRAVEL/COLLIDE数据行改前/改后字节一致(保证指纹)。 - 全文 diff 只落在预期的 8 处锚点内。
4.2 功能验证(浏览器,改前/改后各跑一遍对照)
用 scratch/perf/serve.py(SimpleHTTPRequestHandler,把 /v.html 映射到项目根的真实成品)起 http://localhost:8123/v.html,用浏览器自动化逐项核对并截图:
| 项 | 判据 |
|---|---|
| 首屏加载 | 无 JS 报错;三维模型正常显示 |
| 关节滑块 | 拖动后手臂实时跟随、角度标签同步 |
| 爆炸滑块 / 装配视图按钮 | 模型散开与复位正确 |
| 线框 / 电机半透 / 螺钉 / 可达包络 / 复位 | 逐项开关生效 |
| 装配图页 ↔ 三维页切换 | SVG 显示、切回后三维画布重新渲染(验证"隐藏分支不清标志") |
| 运动模拟页 | 进页即求解并动画到位;IK 表格/徽标/备选构型正常 |
| 姿态控制 | 开关、取当前、垂直向下、箭头拖拽可用 |
| 自检面板 | 显示"指纹 … 匹配";FK/IK/场景图残差仍为 1e-13 mm 量级(验证改动的数学等价性) |
| BOM 螺丝行点击 | 高亮 + 相机对焦 + HUD 文案(昨天的功能) |
| 悬浮 tooltip / 右键隐藏 / H 键 / ESC | 行为一致 |
| 相机预设 / 拖拽旋转 / 滚轮缩放 | 行为一致 |
URL 参数(?view=motion&tgt=…&j1=…&hide=…) |
脚本化视图仍生效 |
4.3 性能前后对比(插桩副本,同一浏览器同一机器)
生成 scratch/perf/measure_before.html(备份 + 尾部追加插桩脚本)与 measure_after.html(改后 + 同一插桩脚本),插桩内容:包装 rend.render 计次与计 CPU 时长、rend.info、performance.memory、导航时序、交互探针(滑块 input、IK.setView('motion')+IK.run() 动画、20 次 pointermove 拾取计时),结果输出到页面固定 div + document.title 以便截图/读取。
预期指标:
| 指标 | 改前(预期) | 改后(预期) |
|---|---|---|
| 空闲 5 s 渲染次数 | ≈300 | ≈0~1 |
| 单次 render CPU 时长 / draw call / 三角面 | 基线 | 相同(画质不变) |
| 20 次 pointermove 拾取总耗时 | ≈26~42 ms | ≈7~11 ms(限流 + 零分配) |
| 索引显存 | 4.08 MB | 2.04 MB |
| 加载(navigation→首次渲染、DOMContentLoaded→load) | 基线 | 持平或略优 |
| 交互后渲染次数(滑块/动画) | — | 每次变化 ≥1 次,无丢帧 |
4.4 回滚
Copy-Item backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html 机器人手臂3D查看器.html(并同步回源文件的 git checkout)。
五、执行顺序
- 备份 + 校验 +
.gitignore(改动 0) - 起静态服务器 + 跑改前功能对照与改前性能插桩(拿到基线数据)
- 改成品 HTML(改动 1→6,逐处 Edit,锚点唯一串)
- 静态校验(4.1)
- 改后功能对照 + 改后性能插桩(4.2/4.3)
- 同步源文件补丁(改动 7)
- 写报告(改动 8)