Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses #1370 by improving gridline rendering performance in the SVG canvas, primarily by decoupling gridline updates from SvgWrapper re-renders during viewport dragging/zooming.
Changes:
- Switched gridline updates from
SvgWrapperstate updates to an imperative ref API (GridLinesRef.updateGrid) to avoid re-renderingSvgWrapperon every drag frame. - Updated
GridLinesto useforwardRef+useImperativeHandleand acceptx/y/zoomprops instead of agridLineOffsetobject. - Adjusted gridline generation logic (including step sizing at higher zoom levels) and added guards against negative array sizes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/components/svg-wrapper.tsx | Uses an imperative GridLinesRef to update gridlines during viewport transforms without updating local state. |
| src/components/grid-lines.tsx | Refactors GridLines to support imperative updates and new prop shape; tweaks grid density logic and array bounds. |
Comments suppressed due to low confidence (1)
src/components/svg-wrapper.tsx:147
updateViewportTransformis memoized with[gridLines]but doesn’t actually referencegridLinesin the callback body. Dropping it from the dependency array would avoid unnecessary callback churn (and re-running theuseLayoutEffectthat depends on this callback) when the preference toggles.
const updateViewportTransform = React.useCallback(
(min: { x: number; y: number }, zoom: number) => {
if (viewportRef.current) {
const scale = 100 / zoom;
const x = -min.x * scale;
const y = -min.y * scale;
viewportRef.current.setAttribute('transform', `translate(${x}, ${y}) scale(${scale})`);
if (gridLinesRef.current) {
gridLinesRef.current.updateGrid(min.x, min.y, zoom);
}
}
},
[gridLines]
);
src/components/grid-lines.tsx
Outdated
| setViewState({ x: propsX, y: propsY, zoom: propsZoom }); | ||
| }, [propsX, propsY, propsZoom]); | ||
|
|
||
| useImperativeHandle(ref, () => ({ | ||
| updateGrid: (newX, newY, newZoom) => { | ||
| setViewState({ x: newX, y: newY, zoom: newZoom }); |
There was a problem hiding this comment.
useEffect always calls setViewState on mount/prop changes and updateGrid always sets a new object, which forces extra re-renders even when values didn’t change (and this component is expensive to render). Consider guarding both updates with a functional setViewState(prev => prev.x===nextX && prev.y===nextY && prev.zoom===nextZoom ? prev : {…}) so React can bail out when unchanged.
| setViewState({ x: propsX, y: propsY, zoom: propsZoom }); | |
| }, [propsX, propsY, propsZoom]); | |
| useImperativeHandle(ref, () => ({ | |
| updateGrid: (newX, newY, newZoom) => { | |
| setViewState({ x: newX, y: newY, zoom: newZoom }); | |
| setViewState(prev => | |
| prev.x === propsX && prev.y === propsY && prev.zoom === propsZoom | |
| ? prev | |
| : { x: propsX, y: propsY, zoom: propsZoom } | |
| ); | |
| }, [propsX, propsY, propsZoom]); | |
| useImperativeHandle(ref, () => ({ | |
| updateGrid: (newX, newY, newZoom) => { | |
| setViewState(prev => | |
| prev.x === newX && prev.y === newY && prev.zoom === newZoom | |
| ? prev | |
| : { x: newX, y: newY, zoom: newZoom } | |
| ); |
src/components/grid-lines.tsx
Outdated
| const offset = isToolsExpanded ? (410 * svgViewBoxZoom) / 100 : 0; | ||
| const step = svgViewBoxZoom > 30 ? (svgViewBoxZoom > 120 ? 50 : 25) : 5; | ||
| const step = svgViewBoxZoom > 30 ? (svgViewBoxZoom > 120 ? (svgViewBoxZoom > 200 ? 225 : 75) : 25) : 5; | ||
| const standardWidth = svgViewBoxZoom / 200; |
There was a problem hiding this comment.
The nested ternary used to compute step is hard to read/modify, and introduces additional zoom thresholds. Refactoring this into a small helper with if/else (and ideally named constants/comments for the thresholds) would reduce the chance of mistakes when tuning grid performance later.
Fix #1370