Interactive charts run in the browser, so data volume directly drives load time, memory and responsiveness.
Default SVG traces create a DOM element per point and slow down beyond tens of thousands of points. Options, roughly in order:
- WebGL traces:
go.Scatterglinstead ofgo.Scatter, orrender_mode="webgl"inpx.scatter/px.line. Express already switches to WebGL automatically above about 1000 points. WebGL handles around a million points, but browsers limit the number of WebGL contexts per page (often 8-16), so pages with many WebGL charts can fail. - Heatmap-style traces:
px.density_heatmap,go.Histogram2dorpx.imshowof a pre-binned grid send a fixed-size grid instead of every point. - Aggregate or sample server-side (pandas
resample, datashader for rasterizing millions of points into an image).
Payload size: Plotly 6+ serializes NumPy arrays as base64 typed arrays ({"dtype": "f8", "bdata": ...}) rather than decimal text, which is smaller and faster to parse. Use compact dtypes (float32, small ints) to shrink them further, and avoid object or string columns where numbers suffice. Unused customdata and long hover texts add up too.
Python-side cost: graph-object validation is thorough but slow for thousands of traces. Build fewer, larger traces; for very large generated figures you can keep the spec as plain dicts and serialize with plotly.io.to_json(spec, validate=False), at the cost of losing error checking. fig.to_dict() copies data, so avoid calling it repeatedly in loops.
Measure with len(fig.to_json()) and the browser's performance tools.
Example
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
rng = np.random.default_rng(0)
x = rng.normal(size=200_000)
y = rng.normal(size=200_000)
fig = px.scatter(x=x, y=y)
print(fig.data[0].type) # scattergl (auto for large data)
j64 = go.Figure(go.Scattergl(x=x, y=y, mode="markers")).to_json()
j32 = go.Figure(go.Scattergl(x=x.astype("float32"), y=y.astype("float32"),
mode="markers")).to_json()
print('"bdata"' in j64) # True: base64 typed arrays
print(round(len(j32) / len(j64), 2)) # about 0.5: float32 halves payload
heat = px.density_heatmap(x=x, y=y, nbinsx=100, nbinsy=100)
print(len(heat.to_json()) < len(j32) / 10) # False: raw points still sent
agg, xe, ye = np.histogram2d(x, y, bins=100)
img = px.imshow(agg.T, origin="lower") # send only a 100x100 grid
print(len(img.to_json()) < len(j32) / 10) # TrueRemember
- Use WebGL traces (
Scattergl,render_mode='webgl') for large point clouds - Aggregate server-side: a 100x100 grid beats a million points
- NumPy arrays serialize as base64 typed arrays; smaller dtypes mean smaller payloads
Watch out for
- Putting dozens of WebGL charts on one page and hitting the browser's WebGL context limit.