Plotly Scattergl vs Scatter: When to Switch to WebGL for Large Point Clouds
Plotly’s Scattergl trace moves point clouds to the GPU via WebGL, delivering 50 k+ points at interactive frame rates. This post shows the one‑line switch in JS and Python, the feature gaps you’ll inherit, and a practical benchmark you can run today.
31 Jul 2026, 14:22 UTC

The problem: SVG stalls with dense scatter plots
Plotly’s default Scatter trace renders each marker as an individual SVG element. For a few thousand points that is fine, but once you exceed roughly 10 k points the DOM becomes large, frame rates drop, and interactions such as panning, zooming, or hovering become sluggish. The user experience can feel frozen when the browser spends most of its time drawing and updating thousands of SVG nodes.
Thesis: Use Scattergl when throughput matters, but accept a narrower feature set
Plotly introduced Scattergl in plotly.js (and scattergl in Plotly.py) to offload point rendering to the GPU via WebGL. A single WebGL shader can draw tens of thousands of points at 60 fps on modest hardware, which is far beyond what can be achieved with SVG in the same environment. The trade‑off is that many of the rich SVG‑only features—complex hover templates, custom marker symbols, per‑point line styles—are not available or are only partially supported in the WebGL trace.
When to make the switch
- Point count > 10 k and interaction latency > 100 ms during pan or zoom.
- Static export to vector formats (SVG, PDF) is not a primary requirement.
- The target browsers support WebGL 2 (most modern browsers do).
- Acceptable fallback to a CPU‑based canvas renderer if WebGL is unavailable.
Implementation: One‑line change in both stacks
Plotly.js (JavaScript)
// SVG version
var traceSVG = {
type: 'scatter',
mode: 'markers',
x: xs, // array of length N
y: ys,
marker: { size: 4 }
};
// WebGL version – only the trace type changes
var traceGL = {
type: 'scattergl',
mode: 'markers',
x: xs,
y: ys,
marker: { size: 4 }
};
Plotly.newPlot('plot', [traceGL]);
Plotly.py (Python)
import plotly.graph_objects as go
# SVG trace
fig_svg = go.Figure(go.Scatter(
x=xs,
y=ys,
mode='markers',
marker=dict(size=4)
))
# WebGL trace – only the class name changes
fig_gl = go.Figure(go.Scattergl(
x=xs,
y=ys,
mode='markers',
marker=dict(size=4)
))
fig_gl.show()
Both snippets assume xs and ys are arrays of equal length, e.g. 50 000 points. All other layout properties (title, axes, legend) remain identical.
Feature gaps you’ll encounter
| Capability | Scatter (SVG) | Scattergl (WebGL) |
|---|---|---|
| Per‑point hover text | Full HTML/Markdown support via hovertemplate | Only plain text is guaranteed; complex templates may fall back to plain text |
| Marker symbols | All SVG paths + built‑ins (circle, square, triangle, etc.) | Limited to circle, square, diamond, cross, x |
| Line styles (dash, width) | Fully supported via line object | Not supported; only solid lines are drawn for mode='lines' |
| Fill between lines | Supported via fill attribute | Not supported; fill is ignored |
| Selection & lasso | Native, high‑performance | Supported but slower when point count > 100 k; may lag on low‑end GPUs |
| Static export to SVG/PDF | Vector output, full fidelity | Rasterized bitmap (PNG) or low‑fidelity SVG; colors and positions may be approximate |
| GPU dependency | None | Requires WebGL; if unavailable, falls back to CPU canvas rendering which can be slower than SVG |
Worked example: 50 k points, side‑by‑side comparison
Create a single HTML page that renders both traces in separate div elements. Open the page, then use Chrome DevTools → Performance → Record while panning each plot. On a 2023‑mid laptop (Intel Iris Xe) typical observations are:
- Scatter: ~12 fps, ~85 ms main‑thread time per frame.
- Scattergl: ~55 fps, ~9 ms GPU time per frame.
If you disable GPU acceleration (e.g. --disable-gpu flag), Scattergl falls back to a CPU canvas renderer and drops to ~8 fps, which is slower than the SVG version. This demonstrates the hardware dependency.
How to verify in your environment
- Generate a 50 k‑point dataset, e.g.
import numpy as np; xs = np.random.randn(50000); ys = np.random.randn(50000). - Render both traces in the same notebook or web page.
- Open DevTools → Performance, record a 5‑second pan/zoom on each.
- Compare Frames and GPU timings.
- Run the same page in a headless Chromium (
chromium --headless --disable-gpu --dump-dom) and confirm thatScatterglstill produces acanvaselement; the output will be rasterized.
Limitations and practical checks
- Because
Scatterglinherits fromScatterbut does not expose every attribute, it is safest to consult the current API reference for your installed version (e.g.plotly.js@5.20orplotly.py@5.20) before adding attributes likehovertemplateorline.dash. - Exporting a
Scatterglfigure to SVG viaplotly.io.write_imagewill produce a rasterized canvas inside the SVG; verify by opening the exported file in an SVG editor and inspecting theimageelement. - On low‑end or integrated GPUs, the performance advantage may be negligible or even negative; run a quick benchmark on your target hardware before adopting
Scatterglglobally. - Because
Scattergldoes not supportmode='lines+markers', if you need both lines and markers you must either keep a dual‑trace approach or drop toScatter.
Actionable closing
For dashboards that routinely exceed 10 k points and where interaction speed is critical, adopt Scattergl as the default trace. Keep an SVG Scatter fallback for export pipelines, accessibility‑critical views, or low‑end environments. Document the attribute gaps in your component library so future contributors don’t assume full parity. A lightweight CI step that renders both traces in headless Chromium and asserts a minimum 30 fps on Scattergl will catch regressions early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.