When Your Plotly Scatter Plot Freezes the Browser: Switching to WebGL with Scattergl
Plotly's default SVG scatter traces freeze the browser at large point counts. Scattergl switches rendering to WebGL with a one-word change — here's what you gain, what you give up, and when downsampling beats both.
09 Jul 2025, 17:49 UTC

You build a scatter plot of sensor readings — half a million points — and the notebook cell finishes fine. Then you open the chart, try to zoom, and the browser tab locks up. The data isn't the problem. The renderer is. The fix is often a one-word change: go.Scatter becomes go.Scattergl, and the same figure goes from unusable to smooth. But that word buys performance at the cost of some features, so it's worth knowing exactly what you're trading.
Why SVG scatter plots fall over
Plotly.py produces interactive HTML charts rendered in the browser by plotly.js. By default, a scatter trace is drawn as SVG: every point becomes an element in the document's DOM tree. SVG gives you crisp, resolution-independent output and the fullest feature support in the library, which is why it's the default.
The catch is that the browser has to manage one DOM node per marker. At a few thousand points this is fine. Somewhere in the tens of thousands, zooming and panning start to lag because every interaction forces the browser to re-layout and repaint a huge tree. At a few hundred thousand points, most tabs simply stop responding. The exact threshold depends on the machine and browser, so treat any number you read — including this one — as a rough guide, not a spec.
What Scattergl changes
go.Scattergl draws the same kind of trace using WebGL, the browser's GPU-accelerated graphics API. Instead of a DOM node per point, points are rasterized onto a canvas by the graphics card. Hundreds of thousands — often millions — of points stay interactive: zoom, pan, and hover all remain responsive where the SVG version would freeze.
You can confirm WebGL is actually active by opening the exported HTML in browser dev tools and checking whether the plot area contains <canvas> elements instead of SVG <path> nodes. That check matters, because some environments — older browsers, disabled hardware acceleration, some remote-desktop setups — can't do WebGL and may fall back or fail outright.
A worked example: 500k points, three ways
Run this locally with Plotly installed (pip install plotly; check your version with pip show plotly, since WebGL feature support varies by release). It writes three HTML files you can open and compare:
import numpy as np
import plotly.graph_objects as go
rng = np.random.default_rng(42)
n = 500_000
x = np.arange(n)
y = np.cumsum(rng.standard_normal(n)) # random-walk time series
# 1. Default SVG scatter — expect sluggish zoom/pan at this size
fig_svg = go.Figure(go.Scatter(x=x, y=y, mode="markers",
marker=dict(size=2)))
fig_svg.write_html("scatter_svg.html")
# 2. WebGL — same call, one word different
fig_gl = go.Figure(go.Scattergl(x=x, y=y, mode="markers",
marker=dict(size=2)))
fig_gl.write_html("scatter_gl.html")
# 3. Downsampled SVG — fewer points, full SVG feature set
step = 50 # keep every 50th point; tune per zoom level in real apps
fig_ds = go.Figure(go.Scatter(x=x[::step], y=y[::step], mode="markers",
marker=dict(size=2)))
fig_ds.write_html("scatter_downsampled.html")
Open each file and try zooming into a dense region. The SVG version should feel heavy or hang; the WebGL version should stay fluid; the downsampled version should feel light because it only ships 10,000 points. Also compare file sizes — the two full-resolution files carry all 500k points as JSON, while the downsampled one is a fraction of the size. I haven't benchmarked these on your hardware, and you should: responsiveness and file size are exactly the numbers your decision depends on.
The trade-offs, honestly stated
WebGL is not a strict upgrade. Three costs to weigh:
- Feature gaps. Scattergl historically supports a smaller set of marker symbols, and some styling options, error-bar configurations, and text rendering behave differently or aren't available. The gaps shift between Plotly versions, so check the Scattergl reference for your installed version and visually compare against the SVG original before committing. Don't assume parity.
- Viewer environment. WebGL depends on the end user's browser and GPU. If your dashboard's audience includes locked-down corporate machines or remote-desktop sessions, verify the chart renders there before shipping.
- Serialization cost stays. WebGL makes rendering fast, but the raw points still travel to the browser as JSON. A million-point figure means a large HTML file and slow page load regardless of renderer. For static exports or dashboards where load time matters, downsampling often beats raw WebGL.
That third point is why the example includes a third option. If the viewer's screen has roughly two thousand horizontal pixels, sending 500,000 x-positions is mostly waste. Server-side aggregation — min-max sampling per pixel column, binning, or tools like Datashader for truly huge data — preserves the visual shape while keeping the SVG feature set and small files. The strongest pattern for dashboards is zoom-aware downsampling: send an aggregated view initially, then fetch finer detail for the visible range as the user zooms.
A decision rule you can apply today
Start with go.Scatter. If profiling in the browser shows interactions lagging and you're above roughly the tens-of-thousands-of-points range, try the one-word switch to go.Scattergl and re-test. If you need styling features WebGL doesn't support in your version, or if file size and load time matter more than full resolution, downsample instead — and consider doing it per zoom level. Whichever you pick, verify with the actual artifact: export with fig.write_html, open it in the browsers your users have, and measure. The renderer choice is a user-facing performance decision, and the only benchmark that counts is the one on their machines.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.