最近整了个点云的项目,遇到些性能问题。本文将会针对这些问题,给出一些小技巧。

在平台上,选择本地的ply点云数据文件展示。效果如下:

发现18MB大小的点云文件,耗时2s+,过长,不能忍,优化之。

算法改进

整体的逻辑:读取文件内容、逐行读取点数据、整理x、y、z、r、g、b,然后threejs渲染显示。
现阶段代码:

// 原始实现
for (let i = 0; i < lines.length; i++) {
  const line = lines[i].trim();
  if (!line) continue;
  const parts = line.split(/\s+/);
  if (parts.length >= 6) {
    const x = parseFloat(parts[0]);
    const y = parseFloat(parts[1]);
    const z = parseFloat(parts[2]);
    const r = parseInt(parts[3], 10);
    const g = parseInt(parts[4], 10);
    const b = parseInt(parts[5], 10);
    // ...
  }
}

从代码不难看出,每行创建新数组,内存分配开销大。25万行 × 6个元素 = 150万个字符串对象。量确实大。

优化后:

// 优化实现
for (let i = 0; i < lines.length; i++) {
  const line = lines[i];
  if (!line || line.length < 10) continue; // 快速跳过
  

  let idx1 = line.indexOf(' ', 0);
  if (idx1 === -1) continue;
  let idx2 = line.indexOf(' ', idx1 + 1);
  if (idx2 === -1) continue;
  let idx3 = line.indexOf(' ', idx2 + 1);
  if (idx3 === -1) continue;
  let idx4 = line.indexOf(' ', idx3 + 1);
  if (idx4 === -1) continue;
  let idx5 = line.indexOf(' ', idx4 + 1);
  if (idx5 === -1) idx5 = line.length;
  
  // 直接截取,避免数组创建
  const x = parseFloat(line.substring(0, idx1));
  const y = parseFloat(line.substring(idx1 + 1, idx2));
  const z = parseFloat(line.substring(idx2 + 1, idx3));
  const r = parseInt(line.substring(idx3 + 1, idx4), 10);
  const g = parseInt(line.substring(idx4 + 1, idx5), 10);
  const b = parseInt(line.substring(idx5 + 1), 10);
  // ...
}
  • indexOf只返回数字,不创建对象
  • 时间复杂度 O(1) 到 O(n),但实际很快
  • substring直接截取,不创建中间数组

内存优化

现阶段代码:

const points = [];
for (let i = 0; i < vertexCount; i++) {
  points.push({ x, y, z, r, g, b });
}

这是一个很小的细节,主要是JavaScript数组的动态扩容机制。他的过程是:创建新数组(2倍大小)、复制所有元素、垃圾回收旧数组。

优化后

const points = new Array(vertexCount);
for (let i = 0; i < vertexCount; i++) {
  points[i] = { x, y, z, r, g, b };  
}

一次性分配,避免多次扩容,无复制操作,无垃圾回收

非阻塞处理

JavaScript是单线程,长时间解析会阻塞UI,导致页面卡顿。解决方案:让出控制权

具体代码

// 假设有12万个点需要解析
const vertexCount = 120000;
const BATCH_SIZE = 50000;
let pointIndex = 0;

for (let i = 0; i < lines.length; i++) {
  // 解析一行数据
  const line = lines[i];
  // ... 解析逻辑 ...
  points[pointIndex] = { x, y, z, r, g, b };
  pointIndex++;
  
  // 每处理50000个点,让出控制权
  if (pointIndex % BATCH_SIZE === 0) {
    // 这行代码做了什么?
    await new Promise(resolve => setTimeout(resolve, 0));
    
    // 执行流程:
    // 1. setTimeout将resolve函数放入事件队列
    // 2. await暂停当前函数执行
    // 3. 主线程释放,浏览器可以:
    //    - 更新DOM(显示进度条)
    //    - 处理点击事件
    //    - 渲染动画
    // 4. 事件循环执行完其他任务后
    // 5. 执行setTimeout的回调(resolve)
    // 6. Promise resolve,继续执行for循环
  }
}

重点是setTimeout(resolve, 0)。将回调函数放入事件队列,延迟0毫秒执行.就是暂停当前函数执行,让出主线程浏览器可以执行其他任务(更新UI、处理事件等)。事件循环执行完其他任务后,再回来继续执行

流式读取

这个之前讲过很多次,不赘述。

最后看效果:

几轮操作下来,耗时缩减为原来的四分之一,效果完美,散花!