Cesium 大场景性能优化:从参数调优到浏览器缓存的完整实践
让三维场景流畅稳定——面向数字孪生、无人机与 GIS 应用。这套优化清单在多个港口级实景三维项目里验证过。
优化的三条基本原则
在动手调参数之前,先建立判断标准。任何优化如果说不清它减少了什么开销、增加了什么成本,就不该做。
1. 减少渲染工作量 —— 更少的顶点、更简单的材质、更少的后处理
2. 降低资源体积 —— 压缩、纹理降采样、按需传输
3. 按需加载与批处理 —— 不该渲染的不渲染,不该单独渲染的不单独渲染
监控和度量贯穿始终。没有度量,优化就是盲改。
快速优化清单(按投入产出比排序)
措施 | 成本 | 收益 | 说明 |
|---|
requestRenderMode: true
| 极低 | 极高 | 消除无意义的逐帧渲染 | scene3DOnly: true
| 极低 | 高 | 剔除 2D / Columbus 模式开销 | 关闭阴影 | 极低 | 高 | 阴影 pass 是双倍几何开销 | 3D Tiles 参数调优 | 低 | 高 | 见下节 | Primitive 批渲染替代 Entity | 中 | 极高 | 上万要素时是数量级差异 | IndexedDB 缓存 | 中 | 高(二次访问) | 分钟级 → 秒级 | Draco / KTX2 压缩 | 中 | 中 | 影响首屏加载 | 交互节流 | 低 | 中 | 减少高频事件重绘 |
前三项通常五分钟做完,能解决大部分"卡顿"投诉。 一、按需渲染:收益最大的单点优化const viewer = new Cesium.Viewer('container', {
scene3DOnly: true, // 剔除 2D / Columbus 模式相关开销
shadows: false, // 根据场景需要决定是否开阴影
terrainProvider: Cesium.createWorldTerrain({ requestVertexNormals: true })
});
// 只有场景有变化时才渲染
viewer.scene.requestRenderMode = true;
viewer.scene.maximumRenderTimeChange = 0.0;
// 需要更新时手动触发
function trigger() { viewer.requestRender(); }
这是整个 Cesium 优化里性价比最高的一项。 Cesium 默认逐帧重绘,即使相机完全静止、没有任何数据变化。如果你有一个"加载完就不动"的展示大屏,开启 requestRenderMode 后 GPU 占用可以从 40% 降到接近 0。 配合 maximumRenderTimeChange = 0.0,只要有任何微小变化(鼠标移动、动画、图层切换)都会触发一次渲染,且不会漏掉。 注意:开启后,如果你有 CallbackProperty 或定时器驱动的动画,必须手动调用 viewer.requestRender(),否则动画会"卡住不动"。这是使用按需渲染最常见的副作用。 二、3D Tiles 调优:LOD 的三根旋钮const tileset = await Cesium.Cesium3DTileset.fromUrl('https://host/tileset.json');
viewer.scene.primitives.add(tileset);
tileset.maximumScreenSpaceError = 2; // 默认 16,调小更精细但更吃性能
tileset.maximumNumberOfLoadedTiles = 256; // 控制内存与并发上限
tileset.skipLevelOfDetail = true; // 先加载低 LOD,加速进入可视状态
tileset.baseScreenSpaceError = 1024;
tileset.skipLevels = 1;
tileset.dynamicScreenSpaceError = true; // 随相机距离动态调整精度
maximumScreenSpaceError:控制细节换性能数值越小越精细,也越吃性能。在某个港口实景三维项目中的实践是设为 2(默认 16),适当牺牲远处模型精度,帧率提升非常明显。 调整的一般区间: 24–64:性能优先的视图 8–16:用性能换更高的视觉保真度 2:适合数据量大、追求流畅的场景
最优值取决于数据集规模、相机距离和目标硬件。 调试 LOD 细化Cesium 提供明确的调试标志: tileset.debugShowBoundingVolume = true; // 每个可见瓦片的边界体积
tileset.debugShowContentBoundingVolume = true; // 内容边界体积
观察要点:放大时注意边界体积在什么时候细分。体积越小越密集 = 细化越早 = 视觉保真度越高但开销越大;体积越大越稀疏 = 细化越晚 = 更流畅但更粗糙。 3D Tiles 检查器viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin);
if (tileset && viewer.cesium3DTilesInspector?.viewModel) {
viewer.cesium3DTilesInspector.viewModel.tileset = tileset;
}
这是调优时最该常驻的工具。 三、Entity vs Primitive:数量级的差异这是新手最容易犯的错误:用 Entity 渲染上万个要素。 Entity 是面向"少量、需交互、需属性面板"的对象。它内部维护场景图节点、有独立的样式对象和事件系统。渲染一万个 Entity 的开销,远大于渲染同等数量的几何。 正确做法是用 Primitive 批量渲染: const instances = polygons.map(positions => new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(positions)
})
}));
const primitive = new Cesium.Primitive({
geometryInstances: instances,
appearance: new Cesium.PerInstanceColorAppearance({ flat: true }),
asynchronous: true // 异步构建,不阻塞 UI
});
viewer.scene.primitives.add(primitive);
经验阈值: 数据量 | 方案 |
|---|
几百 ~ 几千点 | Entity + Billboard / Point 可以接受 | 几万 ~ 几十万 | Primitive + BillboardCollection / PointPrimitiveCollection | 百万级 + | 转 3D Tiles(点云格式) | 海量矢量线 / 面 | 转 3D Tiles,不要用 Entity |
如果你正打算在 Cesium 里画一百万个点,问题不在 Cesium,在你的数据加载方式。 四、IndexedDB 前端缓存:二次访问的关键在一个大范围港口场景项目里,首次加载三维瓦片和纹理需要分钟级。引入 IndexedDB 缓存后,二次访问降到秒级。 // 首次加载后持久化静态资源
async function cacheResources(tilesetUrl) {
const db = await openDB('cesium-cache', 1);
// 遍历已加载的瓦片,将 b3dm / glb / 纹理写入 IndexedDB
// 下次访问优先从本地读取,完全避免重复网络请求与解析
}
为什么收益这么大:三维瓦片的解析(解包 b3dm、构建几何、上传 GPU)是纯 CPU 开销。即使数据已在 HTTP 缓存里,每次会话仍要重新解析一遍。IndexedDB 缓存的是已解析或可快速复用的结构,省掉的正是这块。 这个优化对"用户频繁访问"的业务场景(每天使用的运维平台)价值远大于一次性演示。 五、资源压缩:Draco 与 KTX2编码 | 压缩对象 | 效果 |
|---|
Draco | 几何(顶点位置、法线、UV) | 通常 4:1 以上 | | | |
| KTX2 / Basis | 纹理贴图 | 显存占用大幅下降,同时支持 GPU 压缩格式 |
|---|
// 在 tileset 转换阶段应用 Draco 压缩
// Cesium 自动识别 draco_compression 扩展
需要注意:Draco 会引入解压开销,如果你的瓶颈是 CPU 而不是带宽,需要实测。
六、交互节流
高频交互事件(鼠标移动、实时数据推送)如果不节流,会不断触发重绘和重新计算。
let pending = false;
viewer.canvas.addEventListener('mousemove', () => {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
pending = false;
handleMouseMove();
});
});
对实时轨迹、告警点位这类高频更新数据,把更新合并到动画帧内,而不是每次推送都立即渲染。
七、常见故障排查
瓦片加载失败
按顺序检查:服务器配置、网络连接、CORS 响应头、瓦片 URL 是否可访问。
帧率骤降
用 Cesium Inspector 定位瓶颈:是瓦片数量过多、几何太复杂、还是纹理太大。不要凭感觉猜。
内存泄漏
用 Chrome DevTools 定位。要点:及时销毁不再需要的 Entity。Cesium 的 Entity 不会自动回收。
viewer.entities.remove(entity); // 必须显式移除
数据格式兼容
确认服务器输出的格式与 Cesium 版本兼容,特别是 3D Tiles 1.0 与 1.1 的差异。
八、排查流程
遇到性能问题时,按这个顺序走:
1. 开 Cesium Inspector,看统计面板
↓
2. 帧率低?
├─ 检查瓦片数量是否失控 → 调 SSE / maximumNumberOfLoadedTiles
├─ 检查 Entity 数量 → 改 Primitive 批渲染
└─ 检查是否为按需渲染 → 开 requestRenderMode
↓
3. 首屏慢?
├─ 网络瓶颈 → 压缩 + CDN + 缓存头
└─ 解析瓶颈 → Draco + IndexedDB 缓存
↓
4. 特定操作卡顿?
→ 交互节流 / 计算移出主线程
先测量,再优化。 凭感觉调参数是这个领域最常见的时间浪费。
进阶方向
隐式瓦片(Implicit Tiling)
3D Tiles 1.1 的隐式瓦片用四叉树/八叉树描述层级,配合子树文件,避免了超大 tileset.json。对于超大规模数据集,这是必要的优化。
新格式:Gaussian Splatting
点云泼溅(Splat)在三维场景重建中表现突出。在 CesiumJS 中已有支持方案,可以加载上亿级的泼溅点 LOD 瓦片。但注意它的屏幕空间误差调优逻辑与传统 3D Tiles 不同:
参考资料
CesiumJS 官方文档 — https://cesium.com/learn/cesiumjs/ref-doc/
3D Tiles 官方规范 — https://github.com/CesiumGS/3d-tiles
Cesium Inspector 使用 — https://cesium.com/learn/cesiumjs/ref-doc/CesiumInspector.html
glTF 压缩规范(Draco / KTX2)— https://github.com/KhronosGroup/glTF