|
| 1 | +import React from "react" |
| 2 | +import CodeBlock from "../../components/CodeBlock" |
| 3 | +import PageLayout from "../../components/PageLayout" |
| 4 | +import { Link } from "react-router-dom" |
| 5 | + |
| 6 | +export default function PerformancePage() { |
| 7 | + return ( |
| 8 | + <PageLayout title="Performance" subtitle="Accessor stability, memoization, and rendering efficiency"> |
| 9 | + <section> |
| 10 | + <h2>Accessor Stability</h2> |
| 11 | + <p> |
| 12 | + Semiotic's canvas-based rendering pipeline rebuilds the scene graph when chart |
| 13 | + configuration changes. Every time a parent component re-renders, React creates |
| 14 | + new prop values — including new function objects for inline accessors like{" "} |
| 15 | + <code>{"xAccessor={d => d.time}"}</code>. Without mitigation, each re-render |
| 16 | + triggers a full scene rebuild even when nothing meaningful changed. |
| 17 | + </p> |
| 18 | + <p> |
| 19 | + Semiotic handles this automatically via <strong>accessor equivalence checking</strong>. |
| 20 | + When <code>updateConfig</code> receives new accessor values, it compares them to the |
| 21 | + previous values using <code>accessorsEquivalent()</code>: |
| 22 | + </p> |
| 23 | + <ul> |
| 24 | + <li><strong>String accessors</strong> — compared by value (<code>"x" === "x"</code>)</li> |
| 25 | + <li><strong>Same function reference</strong> — identical (<code>fn === fn</code>)</li> |
| 26 | + <li><strong>Inline arrows with identical source</strong> — compared via <code>.toString()</code></li> |
| 27 | + <li><strong>Different types</strong> — always treated as changed</li> |
| 28 | + </ul> |
| 29 | + <p> |
| 30 | + This means <code>{"xAccessor={d => d.time}"}</code> written inline in JSX will |
| 31 | + <em>not</em> trigger a scene rebuild on re-render, as long as the source code |
| 32 | + is identical each time. However, string accessors remain the most reliable and |
| 33 | + efficient option. |
| 34 | + </p> |
| 35 | + |
| 36 | + <h3>Accessor preference order</h3> |
| 37 | + <p>From most stable to least:</p> |
| 38 | + <CodeBlock>{`// 1. String accessor (best) — always stable |
| 39 | +<LineChart xAccessor="time" yAccessor="value" /> |
| 40 | +
|
| 41 | +// 2. Function defined outside the component — stable reference |
| 42 | +const getX = (d) => d.timestamp / 1000 |
| 43 | +function MyChart() { |
| 44 | + return <LineChart xAccessor={getX} yAccessor="value" /> |
| 45 | +} |
| 46 | +
|
| 47 | +// 3. useCallback — stable across re-renders |
| 48 | +function MyChart({ divisor }) { |
| 49 | + const getX = useCallback((d) => d.timestamp / divisor, [divisor]) |
| 50 | + return <LineChart xAccessor={getX} yAccessor="value" /> |
| 51 | +} |
| 52 | +
|
| 53 | +// 4. Inline arrow (OK) — caught by .toString() heuristic |
| 54 | +<LineChart xAccessor={d => d.timestamp / 1000} yAccessor="value" />`} |
| 55 | + </CodeBlock> |
| 56 | + |
| 57 | + <h3>When .toString() does not help</h3> |
| 58 | + <p> |
| 59 | + The <code>.toString()</code> comparison catches inline arrows with identical source text. |
| 60 | + It does <strong>not</strong> help when the function body is the same but it closes over a |
| 61 | + changing variable: |
| 62 | + </p> |
| 63 | + <CodeBlock>{`// WARNING: multiplier changes but the source text is always "d => d.value * multiplier" |
| 64 | +// .toString() sees them as equal even though behavior changed |
| 65 | +function MyChart({ multiplier }) { |
| 66 | + return <LineChart yAccessor={d => d.value * multiplier} /> |
| 67 | +} |
| 68 | +
|
| 69 | +// FIX: use useCallback with multiplier in the dependency array |
| 70 | +function MyChart({ multiplier }) { |
| 71 | + const getY = useCallback((d) => d.value * multiplier, [multiplier]) |
| 72 | + return <LineChart yAccessor={getY} /> |
| 73 | +}`} |
| 74 | + </CodeBlock> |
| 75 | + |
| 76 | + <h3>diagnoseConfig warning</h3> |
| 77 | + <p> |
| 78 | + The <code>diagnoseConfig</code> utility (and <code>npx semiotic-ai --doctor</code>) |
| 79 | + emits a <code>FUNCTION_ACCESSOR</code> warning when it detects function accessors in |
| 80 | + your props. This is informational — function accessors work fine, but string accessors |
| 81 | + are preferred for maximum stability and clarity. |
| 82 | + </p> |
| 83 | + <CodeBlock>{`import { diagnoseConfig } from "semiotic/ai" |
| 84 | +
|
| 85 | +const result = diagnoseConfig("LineChart", { |
| 86 | + data: myData, |
| 87 | + xAccessor: d => d.time, |
| 88 | + yAccessor: "value", |
| 89 | +}) |
| 90 | +// result.diagnoses includes: |
| 91 | +// { code: "FUNCTION_ACCESSOR", severity: "warning", |
| 92 | +// message: "Function accessor detected: xAccessor. ..." }`} |
| 93 | + </CodeBlock> |
| 94 | + </section> |
| 95 | + |
| 96 | + <section> |
| 97 | + <h2>MCP Rasterized Output</h2> |
| 98 | + <p> |
| 99 | + The Semiotic MCP server's <code>renderChart</code> tool supports both SVG and PNG |
| 100 | + output formats. SVG is the default. PNG output requires the optional{" "} |
| 101 | + <code>sharp</code> package. |
| 102 | + </p> |
| 103 | + <CodeBlock>{`// MCP tool call with format parameter |
| 104 | +{ |
| 105 | + "component": "LineChart", |
| 106 | + "props": { |
| 107 | + "data": [{ "x": 1, "y": 10 }, { "x": 2, "y": 20 }], |
| 108 | + "xAccessor": "x", |
| 109 | + "yAccessor": "y" |
| 110 | + }, |
| 111 | + "format": "png" // "svg" (default) or "png" |
| 112 | +}`} |
| 113 | + </CodeBlock> |
| 114 | + |
| 115 | + <h3>Installing sharp</h3> |
| 116 | + <p> |
| 117 | + PNG output uses <a href="https://sharp.pixelplumbing.com/">sharp</a> for SVG-to-PNG |
| 118 | + conversion. It's listed as an optional dependency — install it when you need PNG: |
| 119 | + </p> |
| 120 | + <CodeBlock>{`npm install sharp`}</CodeBlock> |
| 121 | + <p> |
| 122 | + If <code>sharp</code> is not installed and <code>format: "png"</code> is requested, |
| 123 | + the tool returns the SVG output with an installation hint instead of failing. |
| 124 | + </p> |
| 125 | + |
| 126 | + <h3>Output format</h3> |
| 127 | + <p> |
| 128 | + PNG output is returned as a Base64 data URI string:{" "} |
| 129 | + <code>data:image/png;base64,...</code>. This can be embedded directly in HTML |
| 130 | + or saved to a file by decoding the Base64 payload. |
| 131 | + </p> |
| 132 | + </section> |
| 133 | + |
| 134 | + <section> |
| 135 | + <h2>General Tips</h2> |
| 136 | + <ul> |
| 137 | + <li> |
| 138 | + <strong>Use sub-entries for smaller bundles.</strong>{" "} |
| 139 | + Import from <code>semiotic/xy</code>, <code>semiotic/ordinal</code>, etc. instead |
| 140 | + of the main <code>semiotic</code> entry to tree-shake unused chart types. |
| 141 | + See <Link to="/getting-started">Getting Started</Link>. |
| 142 | + </li> |
| 143 | + <li> |
| 144 | + <strong>Canvas rendering is default.</strong> All Stream Frames and HOC charts |
| 145 | + render to <code><canvas></code> with SVG overlays for annotations and tooltips. |
| 146 | + This gives better performance than pure SVG for large datasets. |
| 147 | + </li> |
| 148 | + <li> |
| 149 | + <strong>Streaming data uses ring buffers.</strong> The{" "} |
| 150 | + <code>windowSize</code> prop controls how many points are kept in memory. |
| 151 | + Older points are evicted automatically. |
| 152 | + </li> |
| 153 | + <li> |
| 154 | + <strong>Push API avoids re-render overhead.</strong> Use{" "} |
| 155 | + <code>ref.current.push(datum)</code> instead of updating a state array. |
| 156 | + See <Link to="/features/realtime-encoding">Realtime Encoding</Link>. |
| 157 | + </li> |
| 158 | + </ul> |
| 159 | + </section> |
| 160 | + </PageLayout> |
| 161 | + ) |
| 162 | +} |
0 commit comments