Skip to content

fix(core-components): draw non-interactive sparklines as SVG instead of an ECharts instance per cell - #3314

Open
timchambers wants to merge 1 commit into
evidence-dev:mainfrom
auxiliopartners:fix/sparkline-echarts-instance-per-cell
Open

fix(core-components): draw non-interactive sparklines as SVG instead of an ECharts instance per cell#3314
timchambers wants to merge 1 commit into
evidence-dev:mainfrom
auxiliopartners:fix/sparkline-echarts-instance-per-cell

Conversation

@timchambers

Copy link
Copy Markdown

Summary

_Sparkline.svelte creates an offscreen ECharts instance for every non-interactive sparkline, purely to call renderToSVGString() — once in onMount, and again in a reactive block that re-runs on every update:

const offscreenContainer = document.createElement('div');
const tempChart = init(offscreenContainer, 'evidence-light', { renderer: 'svg', height, width });
tempChart.setOption(config);
staticSVG = tempChart.renderToSVGString();
tempChart.dispose();

DataTable renders one sparkline per row (TableRow, GroupRow, SubtotalRow and TotalRow all pass interactive="false"), so a table-heavy page creates hundreds of chart instances.

echarts.init() is roughly 30× slower in WebKit than in V8. On a page with ~100 sparkline cells that becomes a single ~24 s uninterruptible main-thread task in Safari — 32.6 s on an iPhone. Chrome runs the same code fast enough that nobody notices.

A non-interactive sparkline has no tooltip, no axis labels and no animation. It doesn't need a chart engine.

The change

  • Adds getSparklinePaths() to sparkline.js: a dependency-free pure function computing the same layout getSparklineConfig() asks ECharts for — xAxis boundaryGap: '2%', yAxis boundaryGap: ['1%','1%'], scale: yScale (a value axis with scale: false always includes zero), connectNulls: false, 1 px line, and the x axis line resting on zero whenever zero is in range.
  • _Sparkline.svelte renders that as an inline <svg> on the non-interactive path.
  • Interactive sparklines are untouched and still use ECharts, tooltips and connectGroup included.
  • config is still computed in both cases, so the exported config prop keeps its contract.

One incidental improvement: the non-interactive branch now emits identical markup on the server and in the browser. Previously SSR prerendered an ECharts SVG and the client immediately discarded it to build another one; now hydration replaces like with like.

Measurements

Same machine, same build, served from a local static server. The control is a variant page holding all SQL blocks and rows constant (6 DataTables / 106 rows / 4 sparkline columns):

before after
macOS Safari 26.6 — longest main-thread block 24.50 s 0.37 s
macOS Safari — page settled 25.17 s 1.06 s
iPhone, iOS 18.7 — longest main-thread block 32.58 s 0.50 s
iPhone — sparklines rendered 40.56 s 4.18 s
Chrome @6× CPU throttle — load, longest task 5.51 s 1.92 s
Chrome @6× CPU throttle — period/data change 0.91 s 0.28 s

The cost is per instance, not per row of data: it scales with the number of sparkline cells rendered.

How this was isolated

Variant pages holding all 41 SQL blocks byte-identical while varying only the rendered components, macOS Safari, 3 repeats each (±0.3 s):

variant DataTables rows sparkline cols implied main-thread block
empty 0 0 0
19 BigValues, no tables 0 0 0
1 table 1 38 1 ~2.8 s
2 tables 2 73 2 ~13.3 s
4 tables 4 85 3 ~17.7 s
6 tables 6 106 4 ~23.2 s
6 tables, sparkline columns removed 6 106 0

Deleting four <Column contentType=sparkline> lines removes a 24-second block and makes the page indistinguishable from an empty one. Roughly 0.25 s per sparkline cell in WebKit.

BigValue is not implicated — 19 BigValues with no DataTables behave like an empty page, even though several use sparkline=. Consistent with a per-instance cost: 4 instances ≈ 1 s (invisible), ~100 instances ≈ 25 s.

Testing

  • New unit tests (sparkline.spec.js, 9 cases): axis inversion, zero anchoring vs yScale, null-driven line breaks, negative series lifting the baseline off the floor, bars hanging off both sides of it, null for nothing-renderable, single-point and flat-series degenerate cases, and date strings as well as Date objects. All pass under the repo's vitest.
  • Output parity: live SVG output was extracted row by row from a before build and an after build of the same page and diffed side by side — visually indistinguishable.
  • In production use: this change (byte-identical) has been running via patch-package against @evidence-dev/core-components@5.4.2 in a multi-tenant Evidence deployment, verified through a full evidence build including its hydration checks.
  • pnpm run format clean; changeset included.

Notes

  • Sparkline data flows in as an array per cell, so getSparklinePaths() is called per cell too — but it is arithmetic on ~24 points, not a chart engine.
  • Happy to adjust naming, file placement, or split the tests differently.

…ECharts instance per cell

_Sparkline.svelte creates an offscreen ECharts instance for every
non-interactive sparkline, purely to call renderToSVGString() — once in
onMount and again in a reactive block on every update. DataTable renders one
sparkline per row, so a table-heavy page creates hundreds of them.

echarts.init() is roughly 30x slower in WebKit than in V8, so ~100 sparkline
cells put Safari into a single ~24s uninterruptible main-thread task (32.6s on
an iPhone). Chrome runs the same code fast enough to hide it.

A non-interactive sparkline has no tooltip, no axis labels and no animation, so
it does not need a chart engine. Add getSparklinePaths() — a dependency-free
function that computes the same layout getSparklineConfig() asks ECharts for —
and render it as an inline SVG. Interactive sparklines are untouched and still
use ECharts.

The non-interactive branch now emits identical markup on the server and in the
browser, so hydration replaces like with like instead of discarding a
prerendered ECharts SVG and rebuilding it.
@timchambers
timchambers requested a deployment to Approval required to run action on external PR August 9, 2026 17:16 — with GitHub Actions Waiting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant