优化 cesium 界面广告牌(billboard)数据量大于 10w +时,地图加载缓慢、卡顿、加载完成后浏览器严重卡顿甚至崩溃问题

前言:

项目之前的设计,billboard 广告牌是绑在 entityCollection 集合下的,为了能够在获取单个实体(entity)时能够获取更多数据信息(entity 能够注入除了它本身的属性之外的属性信息)
entityCollection 集合加上集群聚合功能,数据量临界点在 3w~4w 左右,就会出现界面卡顿。fps 低于 20 并且波动很大,延迟保持在 100ms 左右。数据量低于临界点时,entity 的方式呈现页面还是比较奈斯的
当数据量大于 10w+时,基本上 fps 处于 0-5,延迟大于 200ms,加载数据时延迟直接飙升几千都可能出现,同时(entityCollection 的)数据量过大直接导致浏览器崩溃无法加载

解决方法(primitives 原语集合)

注:

尝试过很多优化的方法,抛开后台接口数据传递处理的优化,只针对前端 cesium 界面的所有优化方法中,记录我找到的效果最好,并且在衔接后续已经完成的其他功能方面开销最小的优化方式
该方法也适合界面显示大量的 pointPrimitiveCollection(点集合)、labelCollection(label 集合)造成的界面卡顿同理可得嘛
我使用的是显示 billboardCollection 广告牌集合

参考方向

前往 primitives 原语集合官网文档

前往 官网相关例子

前往 实现 primitiveCluster 原语集群参考博客

优化方法:primitiveCollection 原语集合与 primitiveCluster 原语集群结合使用优化广告牌显示

在不需要聚合集群这一项功能的情况下,只使用 primitiveCollection 其实就能够完美的解决广告牌 10w+造成的界面卡顿崩溃等问题。建议是不需要聚合功能时,就不要添加 primitiveCluster 原语集群来处理优化。因为在聚合的方法,会监听摄像机的改变事件时刻改变聚合数量状态,反而会出现卡顿情况。应项目需求,添加聚合功能!针对聚合卡顿我做了加定时器的优化处理下面也会贴出来

primitiveCollection 原语集合

primitiveCollection 使用例子

广告牌集合添加代码(如下),其他的集合如 point、label,官方有文档都大同小异

const billboardCollection = viewer.scene.primitives.add(
  new Cesium.BillboardCollection()
);

billboardCollection.add({
  position: Cesium.Cartesian3.fromDegrees(114.49, 41.23, 0),
  width: 38,
  height: 38,
  image: "xxxxx"
});

此时往 billboardCollection 中添加的 billboard 就会直接呈现在界面上,并且能够轻松应对 10w+的数据量。对比之前的添加方式(如下)效果很明显

// 之前的添加方式
const entityCollection = new Cesium.EntityCollection();

const billboard = new Cesium.BillboardGraphics({
  width: 38,
  height: 38,
  image: "xxxxx"
});
const entity = new Cesium.Entity({
  position: Cesium.Cartesian3.fromDegrees(114.49, 41.23, 0),
  billboard: billboard,
  s1: "xxx",
  s2: "xxx",
  s3: "xxx",
  s4: "xxx",
  s5: "xxx"
});
entityCollection.add(entity);

如果你只需要优化数据量大导致界面卡顿崩溃等问题,不用实现聚合功能,到此就完全 OK 了

primitiveCollection 原语集群

primitiveCollection 的聚合功能原生官方并没有提供,在官方文档中只提供了 EntityCluster 方法,来对 entityCollection 集合进行聚合操作。通过 EntityCluster 方法聚合时需要配合 datasource 对象使用,因为在原生的 datasource 对象自身有 clustering 属性(跳转 之前我写的 EntityCluster 聚合博客)。
由于我们直接使用的 Primitive 方式将 billboard 添加到地图中,就跳过了 datasource 的步骤。因此我们需要自己来定义一个 PrimitiveCluster 方法来创建一个 cluster 对象,针对原语集合进行聚合,结合其他博主文档提供的方法,PrimitiveCluster.js 具体实现方法总结如下

往 cesium 的包文件或者依赖文件中添加 PrimitiveCluster 方法

1.添加的路径:

1:npm 包中----- node_modules\cesium\Source\DataSources\PrimitiveCluster.js
2:引入外部文件方式 ---- Source\DataSources\PrimitiveCluster.js

2.复制同目录下 EntityCluster.js 内容到 PrimitiveCluster.js 中

3.文件内全局修改名称,EntityCluster -> PrimitiveCluster、 entityCluster -> primitiveCluster

4.屏蔽大概在 191 行左右 getScreenSpacePositions 方法中的代码块(EntityCluster 中 item.id 指向的就是 entity 实体对象,在 primitiveCollection 中 item.id 为 undefined 会包错)

/* var canClusterLabels =
  primitiveCluster._clusterLabels && defined(item._labelCollection);
var canClusterBillboards =
  primitiveCluster._clusterBillboards && defined(item.id._billboard);
var canClusterPoints =
  primitiveCluster._clusterPoints && defined(item.id._point);
if (canClusterLabels && (canClusterPoints || canClusterBillboards)) {
  continue;
} */

第 4 步时 如果你的业务需求在添加广告牌时需要为广告牌添加唯一的标识 id(如下添加方式),则可以不用屏蔽源代码,添加的 id 能够规避此处报错

billboardCollection.add({
  id: "xxx",
  position: Cesium.Cartesian3.fromDegrees(114.49, 41.23, 0),
  width: 38,
  height: 38,
  image: "xxxxx"
});

5.在 PrimitiveCluster.js 的上级目录(node_modules\cesium\Source\Cesium.js)中找到入口文件 cesium.js,导入 PrimitiveCluster 方法

export { default as PrimitiveCluster } from "./DataSources/PrimitiveCluster.js";

至此 PrimitiveCluster 方法就添加完成,可以直接通过 new Cesium.PrimitiveCluster()的方式来调用

PrimitiveCluster 方法来实现聚合

1.往 scene.primitives 中添加一个用作‘根’的原语集合 primitives

2.创建一个空 billboardCollection 广告牌集合

3.通过 PrimitiveCluster 方法创建一个 cluster 实例对象 primitiveCluster

4.将 primitiveCluster 添加到原语集合 primitives 中

5.配置 primitiveCluster 对象的基本参数(可以不配置有提供默认参数)

6.(重要*)将空 billboardCollection 广告牌集合赋予 primitiveCluster._billboardCollection,手动添加聚合内容

提一下: label、point 集合添加方式一致

primitiveCluster._labelCollection;
primitiveCluster._pointCollection;

7.(重要*)调用_initialize 方法初始化 cluster 实例的事件监听

8.之后就与 datasource 聚合方式的.then 方法一致,只需要将 dataSource.clustering.clusterEvent.addEventListener 换成 primitiveCluster.clusterEvent.addEventListener

如下:

const primitives = viewer.scene.primitives.add(
  new Cesium.PrimitiveCollection()
);
const billboardCollection = new Cesium.BillboardCollection();

const primitiveCluster = new Cesium.PrimitiveCluster();
primitives.add(primitiveCluster);
primitiveCluster.enabled = true; //开启聚合功能
primitiveCluster.pixelRange = 15; //范围
primitiveCluster.minimumClusterSize = 2; //最小聚合数量
primitiveCluster._billboardCollection = billboardCollection;
primitiveCluster._initialize(viewer.scene);

primitiveCluster.clusterEvent.addEventListener(function(
  clusteredEntities,
  cluster
) {
  // ... 处理聚合显示广告牌代码块与dataSource处理方式一致
});

按照上面的方式完成聚合后,往 billboardCollection 集合中添加 billboard 广告牌就会在页面呈现出来并且聚合显示。但是数据量 10w+的情况下,在处理摄像机视角改变的监听事件时会出现卡顿问题。下面贴一下简单优化的方法

优化 PrimitiveCluster 卡顿问题

在 PrimitiveCluster.js 的_initialize 方法中,可以看到原方法使用 createDeclutterCallback 方法创建了一个回调方法,并将这个回调方法添加到了 scene.camera.changed 监听中。因此只要 scene.camera 视角改变,就会执行聚合的处理逻辑方法返回两个参数 clusteredEntities 与 cluster

primitiveCluster.clusterEvent.addEventListener(function(
  clusteredEntities,
  cluster
) {
  // ... 处理聚合显示广告牌代码块与dataSource处理方式一致
});

所以只需要在_initialize 方法加一个防抖的定时器,让它事件处理频率降低就能达到优化的效果。同时暴露 delay 时间参数可以在实例化后进行配置改变

//1.PrimitiveCluster构造函数中添加_delay参数
this._delay = defaultValue(options.delay, 800)

//2.在PrimitiveCluster.prototype拦截器Object.defineProperties方法中添加_delay的访问以及设置方法
delay: {
  get: function () {
    return this._delay;
  },
  set: function (value) {
    this._delay = value;
  },
},

// 3._initialize方法改造
PrimitiveCluster.prototype._initialize = function(scene) {
  this._scene = scene;
  var cluster = createDeclutterCallback(this);
  this._cluster = cluster;
  var _t = null;
  const _self = this;
  this._removeEventListener = scene.camera.changed.addEventListener(function(amount) {
    if (_t) {
      clearTimeout(_t);
      _t = null;
    }
    _t = setTimeout(() => {
      cluster(amount);
    }, _self._delay);
  });
};

到此上文的内容就是关于优化 cesium 界面广告牌(billboard)数据量大于 10w +时,地图加载缓慢、卡顿、加载完成后浏览器严重卡顿等问题的方法思路,下面贴代码记录。

功能代码记录
import * as Cesium from "cesium/Cesium";
import defaultValue from "./core/defaultValue";

/**
 * @_v 引入外部创建的Viewer实例(new Cesium.Viewer(...))
 * @myPrimitives 原语集合,可以包含页面显示的pointPrimitiveCollection、billboardCollection、labelCollection、primitiveCollection、primitiveCluster
 * @myPrimitiveCluster 自定义原语集群
 * @myBillboardCollection 广告牌集合(站点显示的内容数据)
 *
 * @desc 使用primitiveCollection原语集合与primitiveCluster原语集群,处理地图界面显示广告牌billboard数量 > 10w 级时,界面卡顿,浏览器崩溃等问题
 */
class CommomSiteTookit {
  static _v = null;
  myPrimitives = null;
  myPrimitiveCluster = null;
  myBillboardCollection = null;

  constructor() {}

  /**
   * @desc 使用commomSiteTookit实例前,必须先初始化该实例的_v对象
   */
  init(viewer) {
    this._v = viewer;
  }

  /**
   * @param [options] 具有以下属性的对象
   * @param [options.delay=800] 防抖处理定时器的time
   * @param [options.enabled=true] 是否启用集群
   * @param [options.pixelRange=15] 用于扩展屏幕空间包围框的像素范围
   * @param [options.minimumClusterSize=2] 可集群的屏幕空间对象的最小数量
   *
   * @desc 处理原语集合,并实现聚合集群功能方法
   * @return billboardCollection集合,可直接往集合里添加广告牌billboard,呈现在页面上
   */
  load(options = {}) {
    let billboardCollection = new Cesium.BillboardCollection();

    if (Cesium.defined(this.myPrimitives)) {
      this._v.scene.primitives.remove(this.myPrimitives);
    }
    this.myPrimitives = this._v.scene.primitives.add(
      new Cesium.PrimitiveCollection()
    );

    const primitiveCluster = new Cesium.PrimitiveCluster();
    this.myPrimitives.add(primitiveCluster);
    primitiveCluster.delay = defaultValue(options.delay, 800);
    primitiveCluster.enabled = defaultValue(options.enabled, true);
    primitiveCluster.pixelRange = defaultValue(options.pixelRange, 15);
    primitiveCluster.minimumClusterSize = defaultValue(
      options.minimumClusterSize,
      2
    );
    primitiveCluster._billboardCollection = billboardCollection;
    primitiveCluster._initialize(this._v.scene);

    let removeListener;
    let pinBuilder = new Cesium.PinBuilder();
    /* 定义广告牌 fromText(显示文字,颜色,大小) */
    let pin50 = pinBuilder.fromText("50+", Cesium.Color.RED, 40).toDataURL();
    let pin40 = pinBuilder.fromText("40+", Cesium.Color.ORANGE, 40).toDataURL();
    let pin30 = pinBuilder.fromText("30+", Cesium.Color.YELLOW, 40).toDataURL();
    let pin20 = pinBuilder.fromText("20+", Cesium.Color.GREEN, 40).toDataURL();
    let pin10 = pinBuilder.fromText("10+", Cesium.Color.BLUE, 40).toDataURL();
    /* 数量小于十个的聚合广告牌 */
    let singleDigitPins = new Array(8);
    for (let i = 0; i < singleDigitPins.length; ++i) {
      singleDigitPins[i] = pinBuilder
        .fromText("" + (i + 2), Cesium.Color.VIOLET, 40)
        .toDataURL();
    }

    const _ = this;
    function customStyle() {
      if (Cesium.defined(removeListener)) {
        removeListener();
        removeListener = undefined;
      } else {
        removeListener = primitiveCluster.clusterEvent.addEventListener(
          function(clusteredEntities, cluster) {
            cluster.label.show = false;
            cluster.billboard.show = true;
            cluster.billboard.id = cluster.label.id;
            cluster.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
            /* 根据站点(参数)的数量给予对应的广告牌  */
            if (clusteredEntities.length >= 50) {
              cluster.billboard.image = pin50;
            } else if (clusteredEntities.length >= 40) {
              cluster.billboard.image = pin40;
            } else if (clusteredEntities.length >= 30) {
              cluster.billboard.image = pin30;
            } else if (clusteredEntities.length >= 20) {
              cluster.billboard.image = pin20;
            } else if (clusteredEntities.length >= 10) {
              cluster.billboard.image = pin10;
            } else {
              cluster.billboard.image =
                singleDigitPins[clusteredEntities.length - 2];
            }
          }
        );
      }
      // force a re-cluster with the new styling
      let pixelRange = primitiveCluster.pixelRange;
      primitiveCluster.pixelRange = 0;
      primitiveCluster.pixelRange = pixelRange;
      _.myPrimitiveCluster = primitiveCluster;
    }
    this.myBillboardCollection = billboardCollection;
    // start with custom style
    customStyle();
    return billboardCollection;
  }

  /**
   * @params enable bool值控制开启或关闭集群
   * @desc 控制集群生效与否
   */
  enableCluster(enable) {
    if (Cesium.defined(this.myPrimitiveCluster)) {
      this.myPrimitiveCluster.enabled = enable;
    }
  }

  /**
   * @params id 站点ID
   * @return 返回可操作的广告牌[siteBillboard.image = 'xxxx']
   * @desc 根据id在集合中获取指定站点广告牌
   */
  getSiteBillboardById(id) {
    if (!Cesium.defined(this.myBillboardCollection)) return undefined;
    const _b = this.myBillboardCollection;
    const l = _b.length;
    let siteBillboard = undefined;
    for (let i = 0; i < l; i++) {
      if (id == _b.get(i).id) {
        siteBillboard = _b.get(i);
        break;
      }
    }
    return siteBillboard;
  }

  /**
   * @desc 删除所有站点广告牌
   */
  removeAll() {
    if (Cesium.defined(this.myPrimitives)) {
      this._v.scene.primitives.remove(this.myPrimitives);
    }
  }

  /**
   * @params show bool值 控制显示或隐藏
   * @desc 隐藏或显示所有站点广告牌
   */
  showStatus(show = true) {
    this.myPrimitives.show = show;
  }

  /**
   * @desc 根据id删除指定站点广告牌
   */
  remove(id) {
    const billboard = this.getSiteBillboardById(id);
    billboard && this.myBillboardCollection.remove(billboard);
  }

  /**
   * @desc 销毁(目前退出页面时直接viewer销毁)
   */
  destroy() {
    this.myPrimitives = null;
    this.myPrimitiveCluster = null;
    this.myBillboardCollection = null;
    // this._v.scene.primitives.destroy()
  }
}

export default new CommomSiteTookit();

在执行commomSiteTookit.init(viewer)后,加载数据主要的操作在 load 方法中,load 返回的 billboardCollection,可以动态的添加 billboard 数据,直接呈现在界面,代码如下。

const list = ['10w+数据']
const l = list.length
const data = commomSiteTookit.load({
  enabled: true,
  delay: 1200,
  pixelRange: 20
});
for (let i = 0; i < l; i++) {
  data.add({
    image: `xxxx`,
    scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1, 1.5e7, 0.2),
    width: 38, // default: undefined
    height: 38, // default: undefined
    position: Cesium.Cartesian3.fromDegrees(
      list[i].longitude,
      list[i].latitude,
      0
    ),
    id: list[i].id + ""
  });
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐