Skip to content

Commit d4841cc

Browse files
authored
fix(theming): getColor memoization peformance (#1960)
1 parent 5a6c53c commit d4841cc

File tree

5 files changed

+438
-14
lines changed

5 files changed

+438
-14
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Meta, Canvas, Story } from '@storybook/addon-docs';
2+
import { GetColorStory } from './stories/GetColorStory';
3+
import { GetColorV8Story } from './stories/GetColorV8Story';
4+
5+
<Meta title="Packages/Theming/[patterns]" />
6+
7+
# Patterns
8+
9+
The following stories test the performance of the `getColor` and `getColorV8`
10+
functions. The original implementations used `JSON.stringify` for argument
11+
memoization. The updated implementations, introduced in v9.0.1, use `WeakMap`
12+
object comparison to optimize performance.
13+
14+
## `getColor` test
15+
16+
<Canvas>
17+
<Story name="getColor test">{args => <GetColorStory {...args} />}</Story>
18+
</Canvas>
19+
20+
## `getColorV8` test
21+
22+
<Canvas>
23+
<Story name="getColorV8 test">{args => <GetColorV8Story {...args} />}</Story>
24+
</Canvas>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React, { useEffect, useState } from 'react';
9+
import { StoryFn } from '@storybook/react';
10+
import styled, { useTheme } from 'styled-components';
11+
import { getColor } from '@zendeskgarden/react-theming';
12+
import { Button } from '@zendeskgarden/react-buttons';
13+
import { Grid } from '@zendeskgarden/react-grid';
14+
import { Dots } from '@zendeskgarden/react-loaders';
15+
import { Code, Paragraph, Span } from '@zendeskgarden/react-typography';
16+
17+
const StyledColor = styled.div`
18+
display: inline-block;
19+
border-radius: ${p => p.theme.borderRadii.md};
20+
width: 100px;
21+
height: 100px;
22+
`;
23+
24+
export const GetColorStory: StoryFn = () => {
25+
const theme = useTheme();
26+
const [initialColor, setInitialColor] = useState(
27+
getColor({ theme, variable: 'foreground.default' })
28+
);
29+
const [backgroundColor, setBackgroundColor] = useState(initialColor);
30+
const [perf, setPerf] = useState({ milliseconds: 0, calls: 0 });
31+
const BASELINE_NOCACHE = 2780;
32+
const BASELINE_CACHE = 2000;
33+
let CALLS = 0;
34+
35+
const updateColor = (color: string) => {
36+
setTimeout(() => {
37+
setBackgroundColor(color);
38+
}, 1);
39+
40+
CALLS++;
41+
};
42+
43+
const variablesObject = theme.colors.variables[theme.colors.base];
44+
const variables = Object.keys(variablesObject) as (keyof typeof variablesObject)[];
45+
const hues = Object.keys(theme.palette).filter(hue => typeof theme.palette[hue] === 'object');
46+
const offsets = [-300, -200, -100, 100, 200, 300];
47+
const transparencies = Object.keys(theme.opacity);
48+
49+
const testGetColor = () => {
50+
CALLS = 0;
51+
52+
variables.forEach(variable => {
53+
const variableKeys = Object.keys(variablesObject[variable]);
54+
55+
variableKeys.forEach(variableKey => {
56+
const parameters = { theme, variable: `${variable}.${variableKey}` };
57+
58+
updateColor(getColor(parameters));
59+
60+
transparencies.forEach(transparency => {
61+
updateColor(getColor({ ...parameters, transparency: parseInt(transparency, 10) }));
62+
});
63+
});
64+
});
65+
66+
hues.forEach(hue => {
67+
updateColor(getColor({ theme, hue }));
68+
updateColor(getColor({ theme, hue, dark: { hue } }));
69+
updateColor(getColor({ theme, hue, light: { hue } }));
70+
71+
const shades = Object.keys(theme.palette[hue]);
72+
73+
shades.forEach(shade => {
74+
const parameters = { hue, shade: parseInt(shade, 10) };
75+
76+
updateColor(getColor({ theme, ...parameters }));
77+
updateColor(getColor({ theme, hue, dark: parameters }));
78+
updateColor(getColor({ theme, hue, light: parameters }));
79+
80+
offsets.forEach(offset => {
81+
updateColor(getColor({ theme, ...parameters, offset }));
82+
updateColor(getColor({ theme, hue, dark: { ...parameters, offset } }));
83+
updateColor(getColor({ theme, hue, light: { ...parameters, offset } }));
84+
});
85+
86+
transparencies.forEach(_transparency => {
87+
const transparency = parseInt(_transparency, 10);
88+
89+
updateColor(getColor({ theme, ...parameters, transparency }));
90+
updateColor(getColor({ theme, hue, dark: { ...parameters, transparency } }));
91+
updateColor(getColor({ theme, hue, light: { ...parameters, transparency } }));
92+
93+
offsets.forEach(offset => {
94+
updateColor(getColor({ theme, ...parameters, transparency, offset }));
95+
updateColor(getColor({ theme, hue, dark: { ...parameters, transparency, offset } }));
96+
updateColor(getColor({ theme, hue, light: { ...parameters, transparency, offset } }));
97+
});
98+
});
99+
});
100+
});
101+
};
102+
103+
const handleClick = () => {
104+
setPerf({ milliseconds: 0, calls: 0 });
105+
106+
const startTime = performance.now();
107+
108+
testGetColor();
109+
updateColor(initialColor);
110+
111+
const endTime = performance.now();
112+
const milliseconds = Math.round(endTime - startTime);
113+
114+
setPerf({ milliseconds, calls: CALLS });
115+
};
116+
117+
useEffect(() => {
118+
const color = getColor({ theme, variable: 'foreground.default' });
119+
120+
setInitialColor(color);
121+
setBackgroundColor(color);
122+
}, [theme]);
123+
124+
return (
125+
<Grid>
126+
<Grid.Row>
127+
<Grid.Col>
128+
<Button disabled={backgroundColor !== initialColor} onClick={handleClick}>
129+
{backgroundColor === initialColor ? (
130+
'Start'
131+
) : (
132+
<Dots delayMS={0} aria-label="Start" size={theme.space.base * 5} />
133+
)}
134+
</Button>
135+
</Grid.Col>
136+
</Grid.Row>
137+
<Grid.Row style={{ marginTop: 20 }}>
138+
<Grid.Col>
139+
<StyledColor style={{ backgroundColor }} />
140+
<div style={{ marginTop: 12 }}>
141+
<Span isMonospace>{backgroundColor}</Span>
142+
</div>
143+
</Grid.Col>
144+
</Grid.Row>
145+
{perf.milliseconds > 10 && backgroundColor === initialColor && (
146+
<Grid.Row style={{ marginTop: 20 }}>
147+
<Grid.Col>
148+
<Paragraph>
149+
Performed {perf.calls} <Code>getColor</Code> calls in {perf.milliseconds}{' '}
150+
milliseconds.
151+
</Paragraph>
152+
<Paragraph>
153+
Resulted in a{' '}
154+
{`${Math.floor(((BASELINE_NOCACHE - perf.milliseconds) / BASELINE_NOCACHE) * 100)}% (uncached) `}
155+
and{' '}
156+
{`${Math.floor(((BASELINE_CACHE - perf.milliseconds) / BASELINE_CACHE) * 100)}% (cached) `}
157+
improvement over the <Code>JSON.stringify</Code> memoization baseline.
158+
</Paragraph>
159+
</Grid.Col>
160+
</Grid.Row>
161+
)}
162+
</Grid>
163+
);
164+
};
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React, { useEffect, useState } from 'react';
9+
import { StoryFn } from '@storybook/react';
10+
import styled, { useTheme } from 'styled-components';
11+
import { getColorV8 } from '@zendeskgarden/react-theming';
12+
import { Button } from '@zendeskgarden/react-buttons';
13+
import { Grid } from '@zendeskgarden/react-grid';
14+
import { Dots } from '@zendeskgarden/react-loaders';
15+
import { Code, Paragraph, Span } from '@zendeskgarden/react-typography';
16+
import PALETTE_V8 from '../../../src/elements/palette/v8';
17+
18+
const StyledColor = styled.div`
19+
display: inline-block;
20+
border-radius: ${p => p.theme.borderRadii.md};
21+
width: 100px;
22+
height: 100px;
23+
`;
24+
25+
export const GetColorV8Story: StoryFn = () => {
26+
const theme = useTheme();
27+
const [initialColor, setInitialColor] = useState(
28+
getColorV8(theme.colors.base === 'dark' ? 'background' : 'foreground', 600, theme)
29+
);
30+
const [backgroundColor, setBackgroundColor] = useState(initialColor);
31+
const [perf, setPerf] = useState({ milliseconds: 0, calls: 0 });
32+
const BASELINE_NOCACHE = 123;
33+
const BASELINE_CACHE = 107;
34+
let CALLS = 0;
35+
36+
const updateColor = (color?: string) => {
37+
setTimeout(() => {
38+
setBackgroundColor(color);
39+
}, 1);
40+
41+
CALLS++;
42+
};
43+
44+
const hues = Object.keys(PALETTE_V8).filter(
45+
hue => hue !== 'product' && typeof (PALETTE_V8 as any)[hue] === 'object'
46+
);
47+
const shades = [100, 200, 300, 400, 500, 600, 700, 800];
48+
const transparencies = Object.keys(theme.opacity);
49+
50+
const testGetColor = () => {
51+
hues.forEach(hue => {
52+
updateColor(getColorV8(hue, 600));
53+
54+
shades.forEach(shade => {
55+
updateColor(getColorV8(hue, shade));
56+
57+
transparencies.forEach(transparency => {
58+
updateColor(getColorV8(hue, shade, theme, theme.opacity[parseInt(transparency, 10)]));
59+
});
60+
});
61+
});
62+
};
63+
64+
const handleClick = () => {
65+
setPerf({ milliseconds: 0, calls: 0 });
66+
67+
const startTime = performance.now();
68+
69+
testGetColor();
70+
updateColor(initialColor);
71+
72+
const endTime = performance.now();
73+
const milliseconds = Math.round(endTime - startTime);
74+
75+
setPerf({ milliseconds, calls: CALLS });
76+
};
77+
78+
useEffect(() => {
79+
const color = getColorV8(
80+
theme.colors.base === 'dark' ? 'background' : 'foreground',
81+
600,
82+
theme
83+
);
84+
85+
setInitialColor(color);
86+
setBackgroundColor(color);
87+
}, [theme]);
88+
89+
return (
90+
<Grid>
91+
<Grid.Row>
92+
<Grid.Col>
93+
<Button disabled={backgroundColor !== initialColor} onClick={handleClick}>
94+
{backgroundColor === initialColor ? (
95+
'Start'
96+
) : (
97+
<Dots delayMS={0} aria-label="Start" size={theme.space.base * 5} />
98+
)}
99+
</Button>
100+
</Grid.Col>
101+
</Grid.Row>
102+
<Grid.Row style={{ marginTop: 20 }}>
103+
<Grid.Col>
104+
<StyledColor style={{ backgroundColor }} />
105+
<div style={{ marginTop: 12 }}>
106+
<Span isMonospace>{backgroundColor}</Span>
107+
</div>
108+
</Grid.Col>
109+
</Grid.Row>
110+
{perf.milliseconds > 1 && backgroundColor === initialColor && (
111+
<Grid.Row style={{ marginTop: 20 }}>
112+
<Grid.Col>
113+
<Paragraph>
114+
Performed {perf.calls} <Code>getColorV8</Code> calls in {perf.milliseconds}{' '}
115+
milliseconds.
116+
</Paragraph>
117+
<Paragraph>
118+
Resulted in a{' '}
119+
{`${Math.floor(((BASELINE_NOCACHE - perf.milliseconds) / BASELINE_NOCACHE) * 100)}% (uncached) `}
120+
and{' '}
121+
{`${Math.floor(((BASELINE_CACHE - perf.milliseconds) / BASELINE_CACHE) * 100)}% (cached) `}
122+
improvement over the <Code>JSON.stringify</Code> memoization baseline.
123+
</Paragraph>
124+
</Grid.Col>
125+
</Grid.Row>
126+
)}
127+
</Grid>
128+
);
129+
};

0 commit comments

Comments
 (0)