# Speed up slow figure updates in a Dash app

Classify where figure-update time goes (whole-figure network payloads, full redraws on streaming ticks, SVG rendering of large traces, lost zoom state, or server round trips for presentation-only changes) and apply the matching fix: dash.Patch partial updates, extendData/prependData streaming, Scattergl WebGL rendering, layout uirevision, or clientside callbacks.

Exact reference: {"kind":"skill_version","skill_id":"skl_smLKiFihYJfszPZ-fDI8Xw","version_id":"skv_wMej8zhNnlDaSlQAldgaGA"}

Applicability: [{"constraint":">=2.9","technology":"Dash","version_scheme":"semver"},{"constraint":">=5.0","technology":"plotly.py","version_scheme":"semver"}]

# Speed up slow figure updates in a Dash app

Use this when a `dcc.Graph` update feels sluggish: the browser freezes, the
page goes unresponsive for seconds, every interaction (dropdown, slider, hover)
triggers a slow redraw, or zoom/pan resets every time the figure refreshes.
Classify WHERE the time goes before optimizing — the fix for network
round-trips, the fix for browser rendering, and the fix for lost view state are
three different things.

## 1. Classify the bottleneck first

- **Whole figure crosses the wire every update.** The callback returns a full
  `figure` dict, and the update takes roughly as long over loopback as over
  the network. Dev-tools Network tab shows a large `_dash-update-component`
  response on every interaction. Fix with partial updates (step 2).
- **Streaming/live data redraws everything each tick.** An
  `dcc.Interval`-driven callback rebuilds and returns the entire figure to
  append one point. Fix with `extendData` (step 3).
- **Browser rendering is the bottleneck.** Updates arrive fast (check the Dash
  dev-tools callback timing) but the tab stutters or freezes, especially with
  tens of thousands of points. The default `scatter` trace renders in SVG —
  fix with WebGL (step 4).
- **Zoom, pan, or legend toggles reset on every update.** The figure redraws
  fine, but the user's view state is lost. Fix with `uirevision` (step 5).
- **A server round trip happens for something the browser already knows.**
  E.g. toggling trace visibility, recoloring, or restyling on hover — the data
  is already in the browser, only presentation changes. Fix with a clientside
  callback (step 6).

## 2. Stop re-sending the whole figure: `Patch` (Dash 2.9+)

When only part of the figure changes (a title, an axis type, a trace's color),
send only the change. `Patch` operations execute in the browser:

```python
from dash import Patch, Input, Output, callback

@callback(
    Output("graph", "figure"),
    Input("scale", "value"),
)
def update_scale(scale):
    patched = Patch()
    patched["layout"]["yaxis"]["type"] = scale          # assign a value
    patched["layout"]["title"]["text"] = "New title"    # merge into dict
    patched["data"][0]["marker"]["color"] = "red"       # touch one trace
    patched["layout"]["annotations"].append(new_anno)   # append to a list
    return patched
```

Import it with `from dash import Patch`. This is the default first move: most
"slow figure update" callbacks rebuild megabytes of unchanged data because
returning the whole figure is the easy habit.

## 3. Stream points without redrawing: `extendData`

For live/interval updates, append instead of replace. The `extendData` prop
takes `[updateData, traceIndices, maxPoints]`, mirroring the
`Plotly.extendTraces` API:

```python
from dash import Input, Output, callback
import plotly.graph_objects as go

app.layout = [
    dcc.Graph(
        id="live",
        figure=go.Figure(
            data=[go.Scattergl(x=[], y=[], mode="lines")],
            layout={"xaxis": {"type": "date"}, "uirevision": "live"},
        ),
    ),
    dcc.Interval(id="tick", interval=1000),
]

@callback(
    Output("live", "extendData"),
    Input("tick", "n_intervals"),
)
def append_point(n):
    x, y = read_latest_measurement()
    # [data-to-append, trace indices, max points kept]
    return [{"x": [[x]], "y": [[y]]}, [0], 1000]
```

Notes: `updateData` values are lists-of-lists (one list per extended trace);
`maxPoints` caps the trace length so a long-running stream doesn't eat the
browser's memory. There is also `prependData` with the same shape. If
`extendData` is an output, do not also output `figure` from the same callback.

## 4. Render large data with WebGL: `Scattergl`

The plain `scatter` trace type renders in SVG, which degrades badly past tens
of thousands of points. Switch large scatter/line traces to the WebGL-backed
equivalent — usually a one-word change:

```python
import plotly.graph_objects as go

go.Scattergl(x=xs, y=ys, mode="lines")   # instead of go.Scatter(...)
```

Keep the rest of the figure identical. If you pre-aggregate for display anyway
(histograms, binned heatmaps), do the aggregation server-side and send the
small result — no rendering trick beats sending less data.

## 5. Preserve zoom/pan across updates: `uirevision`

By default, returning a new figure resets the user's zoom and pan. Set
`uirevision` on the layout to a constant string: as long as the value is
unchanged between updates, plotly.js keeps the current view:

```python
fig.update_layout(uirevision="constant")
```

Change the value only when you *want* the view to reset (e.g. a "reset view"
button sets a new revision string). Common mistake: omitting `uirevision`
while updating `figure` on a timer, which makes zooming impossible because
every tick redraws and resets.

## 6. Move presentation-only logic to the browser: clientside callbacks

If the data is already in the browser and the callback only restyles,
filters client-side, or reformats, skip the server round trip entirely:

```python
from dash import clientside_callback, Input, Output, State

clientside_callback(
    """
    function(scale, fig) {
        fig.layout.yaxis.type = scale;
        return fig;
    }
    """,
    Output("graph", "figure"),
    Input("scale", "value"),
    State("graph", "figure"),
)
```

JavaScript runs in the browser with no HTTP request. Inside the JS, return
`window.dash_clientside.no_update` to skip the update. Clientside callbacks
cannot run Python, so they are for presentation and light transforms only —
not for querying data.

## 7. Checklist for a slow figure

1. Time it: Dash dev-tools callback panel tells you server time vs total time.
2. If the `_dash-update-component` payload is large → `Patch` (or restructure
   the callback to return less).
3. If it's a streaming tick → `extendData` with `maxPoints` set.
4. If the browser still stutters on render → `Scattergl`, and aggregate
   server-side where possible.
5. If zoom resets on update → `uirevision` constant on the layout.
6. If the callback only restyles known data → clientside callback.


## Supporting basis and limitations

Built from the Dash documentation (Partial Property Updates: Patch new in Dash 2.9, operations executed in the browser; dcc.Graph reference: extendData/prependData as [updateData, traceIndices, maxPoints] mirroring Plotly.extendTraces; Dash 2.13+ plotly.js versioning) and recurring Plotly community forum threads on extendData streaming and clientside hover performance.

## Change and rationale

New skill: speed up slow figure updates in a Dash app.

Slow-figure questions recur on the Plotly forum, and most answers prescribe one technique (usually 'use clientside callbacks') without diagnosing where the time actually goes. This skill adds a classify-first decision procedure across the five real bottlenecks — payload size, redraw-on-stream, SVG rendering, view-state loss, and unnecessary round trips — each with the documented Dash mechanism that addresses it.
