Performance Tips

Viser is Usually Fast™, but can require care for larger scenes and time-series data.

There are a few things to be mindful of.

Scene Node Count

Scenes in Viser can contain hundreds of objects without problems. When we start visualizing thousands, however, the viewer can start to choke.

This is true even for simple geometries. For example, a scene with 5000 boxes will start to sputter on most machines:

import viser

server = viser.ViserServer()

# Slow... 👎
for i in range(5000):
    server.scene.add_box(
        f"/box_{i}",
        dimensions=(0.1, 0.1, 0.1),
        position=np.random.normal(size=3),
    )
server.sleep_forever()

Batching objects can help by reducing CPU overhead and WebGL draw calls. Viser provides several methods for batching, including:

In the case of splines and line segments, multiple lines can also be combined into single add_line_segments() calls.

With batching, even tens of thousands of boxes should feel snappy:

import numpy as np
import trimesh
import viser

server = viser.ViserServer()

N = 50_000
batched_wxyzs = np.broadcast_to(np.array([1.0, 0.0, 0.0, 0.0]), (N, 4))
batched_positions = np.random.normal(size=(N, 3))

# Much faster! 👍
server.scene.add_batched_meshes_trimesh(
    "/boxes",
    mesh=trimesh.creation.box(extents=(0.1, 0.1, 0.1)),
    batched_wxyzs=batched_wxyzs,
    batched_positions=batched_positions,
)
server.sleep_forever()

Time-series Data

The typical pattern for animations in Viser is to (1) set up the scene and (2) update it in a loop:

import viser

server = viser.ViserServer()

# Scene setup.
pass

while True:
    # Update the scene.
    pass

    # Sleep based on update rate.
    time.sleep(1.0 / 30.0)

Compared to native viewers, one limitation of Viser is transport overhead. 3D data is serialized in Python, passed through a websocket connection, deserialized in your web browser, and then rendered using WebGL. These steps are not typically an issue for static visualizations or the “setup” stage in the example above. When combined with larger assets in the “update” stage and faster update rates, however, they can become a bottleneck.

For smoother animations, we recommend avoiding heavier operations in loops:

  • ❌ Sending large meshes or point clouds.

  • ❌ Sending large images.

  • ⚠️ Creating new scene nodes. (case-dependent)

Smaller property updates are generally fine. A non-exhaustive list:

For animating heavier assets like point clouds, one workaround for transport limitations is buffering: sending all point cloud data at the start and then only toggling visibilities in the update loop.

Note

Setting the visibility of a parent scene node to False will also hide all of its children. For time-series data, you can create a parent node for each timestep and toggle visibility of the parent to show/hide all children without looping through them individually. For an example of this pattern, see the Record3D visualizer.

Image Encoding Overhead

Images in Viser are represented as NumPy arrays and encoded for transport using either JPEG or PNG compression. JPEG is generally faster, but PNG is lossless and supports transparency.

If you run into problems with frequent updates to properties like viser.ImageHandle.image and viser.CameraFrustumHandle.image, or calls to viser.SceneApi.set_background_image(), we recommend:

  • Downsizing images before sending them.

  • Using JPEG encoding if possible. Encoding can generally be set via a format= keyword argument.

  • Ensuring that opencv-python is installed. This isn’t a strict dependency of Viser, but Viser will use it to accelerate image encoding if installed. See discussion and benchmarks on GitHub.