Skip to content

Commit 0b9b7c6

Browse files
committed
fix(Canvas): Improve performace by reducing computed style lookups and memoizing responses
1 parent 0b0a459 commit 0b9b7c6

2 files changed

Lines changed: 61 additions & 5 deletions

File tree

.changeset/tricky-nights-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
fix(Canvas): Improve performace by reducing computed style lookups and memoizing responses

packages/layerchart/src/lib/utils/canvas.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,28 @@ export type ComputedStylesOptions = {
2020
classes?: ClassValue | null;
2121
};
2222

23+
const supportedStyles = [
24+
'fill',
25+
'stroke',
26+
'opacity',
27+
'fillOpacity',
28+
'strokeWidth',
29+
'fontWeight',
30+
'fontSize',
31+
'fontFamily',
32+
'textAnchor',
33+
'textAlign',
34+
'paintOrder',
35+
] as const;
36+
2337
/**
2438
* Appends or reuses `<svg>` element below `<canvas>` to resolve CSS variables and classes (ex. `stroke: var(--color-primary)` => `stroke: rgb(...)` )
2539
*/
26-
export function getComputedStyles(
40+
export function _getComputedStyles(
2741
canvas: HTMLCanvasElement,
2842
{ styles, classes }: ComputedStylesOptions = {}
2943
) {
44+
// console.count(`getComputedStyles: ${getComputedStylesKey(canvas, { styles, classes })}`);
3045
try {
3146
// Get or create `<svg>` below `<canvas>`
3247
let svg = document.getElementById(CANVAS_STYLES_ELEMENT_ID) as SVGElement | null;
@@ -61,14 +76,32 @@ export function getComputedStyles(
6176
);
6277
}
6378

64-
const computedStyles = window.getComputedStyle(svg);
79+
// Capture copy to enable memoization and avoid capturing all styles (which is very slow)
80+
const computedStyles = supportedStyles.reduce((acc, style) => {
81+
acc[style] = window.getComputedStyle(svg)[style];
82+
return acc;
83+
}, {} as CSSStyleDeclaration);
84+
6585
return computedStyles;
6686
} catch (e) {
6787
console.error('Unable to get computed styles', e);
6888
return {} as CSSStyleDeclaration;
6989
}
7090
}
7191

92+
function getComputedStylesKey(
93+
canvas: HTMLCanvasElement,
94+
{ styles, classes }: ComputedStylesOptions = {}
95+
) {
96+
return JSON.stringify({ canvasId: canvas.id, styles, classes });
97+
}
98+
99+
export const getComputedStyles = memoize(_getComputedStyles, {
100+
cacheKey: ([canvas, styleOptions]) => {
101+
return getComputedStylesKey(canvas, styleOptions);
102+
},
103+
});
104+
72105
/** Render onto canvas context. Supports CSS variables and classes by tranferring to hidden `<svg>` element before retrieval) */
73106
function render(
74107
ctx: CanvasRenderingContext2D,
@@ -89,11 +122,29 @@ function render(
89122
)
90123
) {
91124
// Skip resolving styles if no classes are provided and no styles are using CSS variables
92-
// TODO: Convert colors using `rgb(0 0 0 / 50%)` to `rgba(0, 0, 0, 0.5)`
93125
resolvedStyles = styleOptions.styles ?? {};
94126
} else {
95-
const computedStyles = getComputedStyles(ctx.canvas, styleOptions);
96-
resolvedStyles = computedStyles;
127+
// Remove constant non-css variable properties (ex. `strokeWidth: 0.5`, `fill: #123456`) as not needed and improves memoization cache hit
128+
const { constantStyles, variableStyles } = Object.entries(styleOptions.styles ?? {}).reduce<{
129+
constantStyles: StyleOptions;
130+
variableStyles: StyleOptions;
131+
}>(
132+
(acc, [key, value]) => {
133+
if (typeof value === 'number' || (typeof value === 'string' && !value.includes('var('))) {
134+
(acc.constantStyles as any)[key] = value;
135+
} else if (typeof value === 'string' && value.includes('var(')) {
136+
(acc.variableStyles as any)[key] = value;
137+
}
138+
return acc;
139+
},
140+
{ constantStyles: {} as StyleOptions, variableStyles: {} as StyleOptions }
141+
);
142+
143+
const computedStyles = getComputedStyles(ctx.canvas, {
144+
styles: variableStyles,
145+
classes: styleOptions.classes,
146+
});
147+
resolvedStyles = { ...computedStyles, ...constantStyles };
97148
}
98149

99150
// Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order

0 commit comments

Comments
 (0)