Skip to content

Commit 719856f

Browse files
authored
fix(clerk-js): Reduce unnecessary re-renders on CheckoutComplete (#6765)
1 parent 8824e7d commit 719856f

2 files changed

Lines changed: 151 additions & 133 deletions

File tree

.changeset/tangy-snails-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Performance improvements for `<Checkout />`.

packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx

Lines changed: 146 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,160 @@ import { useRouter } from '../../router';
1414
const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1);
1515
const lerp = (start: number, end: number, amt: number) => start + (end - start) * amt;
1616

17+
const SuccessRing = ({ positionX, positionY }: { positionX: number; positionY: number }) => {
18+
const animationRef = useRef<number | null>(null);
19+
const [currentPosition, setCurrentPosition] = useState({ x: 256, y: 256 });
20+
21+
const canHover =
22+
typeof window === 'undefined' ? true : window.matchMedia('(hover: hover) and (pointer: fine)').matches;
23+
24+
useEffect(() => {
25+
if (!canHover) {
26+
return;
27+
}
28+
const animate = () => {
29+
setCurrentPosition(prev => {
30+
const amt = 0.15;
31+
const x = lerp(prev.x, positionX, amt);
32+
const y = lerp(prev.y, positionY, amt);
33+
return { x, y };
34+
});
35+
animationRef.current = requestAnimationFrame(animate);
36+
};
37+
animationRef.current = requestAnimationFrame(animate);
38+
return () => {
39+
if (animationRef.current) {
40+
cancelAnimationFrame(animationRef.current);
41+
}
42+
};
43+
}, [positionX, positionY, canHover]);
44+
45+
// Generate unique IDs for SVG elements to avoid conflicts with multiple component instances
46+
const maskId1 = useId();
47+
const maskId2 = useId();
48+
const maskId3 = useId();
49+
50+
return (
51+
<Box
52+
elementDescriptor={descriptors.checkoutSuccessRings}
53+
as='svg'
54+
// @ts-ignore - viewBox is a valid prop for svg
55+
viewBox='0 0 512 512'
56+
sx={{
57+
position: 'absolute',
58+
inset: 0,
59+
pointerEvents: 'none',
60+
}}
61+
aria-hidden
62+
>
63+
<defs>
64+
<radialGradient id='clerk-checkout-success-gradient'>
65+
<stop
66+
offset='0%'
67+
style={{
68+
stopColor: 'var(--ring-highlight)',
69+
}}
70+
/>
71+
<stop
72+
offset='100%'
73+
stopOpacity='0'
74+
style={{
75+
stopColor: 'var(--ring-highlight)',
76+
}}
77+
/>
78+
</radialGradient>
79+
<filter id='clerk-checkout-success-blur-effect'>
80+
<feGaussianBlur stdDeviation='10' />
81+
</filter>
82+
{[
83+
{ r: 225, maskStart: 10, maskEnd: 90, id: maskId1 },
84+
{ r: 162.5, maskStart: 15, maskEnd: 85, id: maskId2 },
85+
{ r: 100, maskStart: 20, maskEnd: 80, id: maskId3 },
86+
].map(({ maskStart, maskEnd, id }) => (
87+
<linearGradient
88+
key={id}
89+
id={`gradient-${id}`}
90+
x1='0%'
91+
y1='0%'
92+
x2='0%'
93+
y2='100%'
94+
>
95+
<stop
96+
offset={`${maskStart + 5}%`}
97+
stopColor='white'
98+
stopOpacity='0'
99+
/>
100+
<stop
101+
offset={`${maskStart + 35}%`}
102+
stopColor='white'
103+
stopOpacity='1'
104+
/>
105+
<stop
106+
offset={`${maskEnd - 35}%`}
107+
stopColor='white'
108+
stopOpacity='1'
109+
/>
110+
<stop
111+
offset={`${maskEnd - 5}%`}
112+
stopColor='white'
113+
stopOpacity='0'
114+
/>
115+
</linearGradient>
116+
))}
117+
<mask id='clerk-checkout-success-mask'>
118+
{[
119+
{ r: 225, id: maskId1 },
120+
{ r: 162.5, id: maskId2 },
121+
{ r: 100, id: maskId3 },
122+
].map(({ r, id }) => (
123+
<circle
124+
key={id}
125+
cx='256'
126+
cy='256'
127+
r={r}
128+
stroke={`url(#gradient-${id})`}
129+
fill='none'
130+
strokeWidth='1'
131+
/>
132+
))}
133+
</mask>
134+
</defs>
135+
<g mask='url(#clerk-checkout-success-mask)'>
136+
<rect
137+
width='512'
138+
height='512'
139+
style={{
140+
fill: 'var(--ring-fill)',
141+
}}
142+
/>
143+
{canHover && (
144+
<rect
145+
id='movingGradientHighlight'
146+
width='256'
147+
height='256'
148+
x={currentPosition.x - 128}
149+
y={currentPosition.y - 128}
150+
fill='url(#clerk-checkout-success-gradient)'
151+
filter='url(#clerk-checkout-success-blur-effect)'
152+
/>
153+
)}
154+
</g>
155+
</Box>
156+
);
157+
};
158+
17159
export const CheckoutComplete = () => {
18160
const router = useRouter();
19161
const { setIsOpen } = useDrawerContext();
20162
const { newSubscriptionRedirectUrl } = useCheckoutContext();
21163
const { checkout } = useCheckout();
22164
const { totals, paymentSource, planPeriodStart, freeTrialEndsAt } = checkout;
23165
const [mousePosition, setMousePosition] = useState({ x: 256, y: 256 });
24-
const [currentPosition, setCurrentPosition] = useState({ x: 256, y: 256 });
25-
26-
// Generate unique IDs for SVG elements to avoid conflicts with multiple component instances
27-
const maskId1 = useId();
28-
const maskId2 = useId();
29-
const maskId3 = useId();
30166

31167
const prefersReducedMotion = usePrefersReducedMotion();
32168
const { animations: layoutAnimations } = useAppearance().parsedLayout;
33169
const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
34170

35-
const animationRef = useRef<number | null>(null);
36171
const checkoutSuccessRootRef = useRef<HTMLSpanElement>(null);
37172
const canHover =
38173
typeof window === 'undefined' ? true : window.matchMedia('(hover: hover) and (pointer: fine)').matches;
@@ -59,27 +194,6 @@ export const CheckoutComplete = () => {
59194
}
60195
};
61196

62-
useEffect(() => {
63-
if (!canHover) {
64-
return;
65-
}
66-
const animate = () => {
67-
setCurrentPosition(prev => {
68-
const amt = 0.15;
69-
const x = lerp(prev.x, mousePosition.x, amt);
70-
const y = lerp(prev.y, mousePosition.y, amt);
71-
return { x, y };
72-
});
73-
animationRef.current = requestAnimationFrame(animate);
74-
};
75-
animationRef.current = requestAnimationFrame(animate);
76-
return () => {
77-
if (animationRef.current) {
78-
cancelAnimationFrame(animationRef.current);
79-
}
80-
};
81-
}, [mousePosition, canHover]);
82-
83197
const handleClose = () => {
84198
if (newSubscriptionRedirectUrl) {
85199
void router.navigate(newSubscriptionRedirectUrl);
@@ -135,111 +249,10 @@ export const CheckoutComplete = () => {
135249
ref={checkoutSuccessRootRef}
136250
onMouseMove={handleMouseMove}
137251
>
138-
<Box
139-
elementDescriptor={descriptors.checkoutSuccessRings}
140-
as='svg'
141-
// @ts-ignore - viewBox is a valid prop for svg
142-
viewBox='0 0 512 512'
143-
sx={{
144-
position: 'absolute',
145-
inset: 0,
146-
pointerEvents: 'none',
147-
}}
148-
aria-hidden
149-
>
150-
<defs>
151-
<radialGradient id='clerk-checkout-success-gradient'>
152-
<stop
153-
offset='0%'
154-
style={{
155-
stopColor: 'var(--ring-highlight)',
156-
}}
157-
/>
158-
<stop
159-
offset='100%'
160-
stopOpacity='0'
161-
style={{
162-
stopColor: 'var(--ring-highlight)',
163-
}}
164-
/>
165-
</radialGradient>
166-
<filter id='clerk-checkout-success-blur-effect'>
167-
<feGaussianBlur stdDeviation='10' />
168-
</filter>
169-
{[
170-
{ r: 225, maskStart: 10, maskEnd: 90, id: maskId1 },
171-
{ r: 162.5, maskStart: 15, maskEnd: 85, id: maskId2 },
172-
{ r: 100, maskStart: 20, maskEnd: 80, id: maskId3 },
173-
].map(({ maskStart, maskEnd, id }) => (
174-
<linearGradient
175-
key={id}
176-
id={`gradient-${id}`}
177-
x1='0%'
178-
y1='0%'
179-
x2='0%'
180-
y2='100%'
181-
>
182-
<stop
183-
offset={`${maskStart + 5}%`}
184-
stopColor='white'
185-
stopOpacity='0'
186-
/>
187-
<stop
188-
offset={`${maskStart + 35}%`}
189-
stopColor='white'
190-
stopOpacity='1'
191-
/>
192-
<stop
193-
offset={`${maskEnd - 35}%`}
194-
stopColor='white'
195-
stopOpacity='1'
196-
/>
197-
<stop
198-
offset={`${maskEnd - 5}%`}
199-
stopColor='white'
200-
stopOpacity='0'
201-
/>
202-
</linearGradient>
203-
))}
204-
<mask id='clerk-checkout-success-mask'>
205-
{[
206-
{ r: 225, id: maskId1 },
207-
{ r: 162.5, id: maskId2 },
208-
{ r: 100, id: maskId3 },
209-
].map(({ r, id }) => (
210-
<circle
211-
key={id}
212-
cx='256'
213-
cy='256'
214-
r={r}
215-
stroke={`url(#gradient-${id})`}
216-
fill='none'
217-
strokeWidth='1'
218-
/>
219-
))}
220-
</mask>
221-
</defs>
222-
<g mask='url(#clerk-checkout-success-mask)'>
223-
<rect
224-
width='512'
225-
height='512'
226-
style={{
227-
fill: 'var(--ring-fill)',
228-
}}
229-
/>
230-
{canHover && (
231-
<rect
232-
id='movingGradientHighlight'
233-
width='256'
234-
height='256'
235-
x={currentPosition.x - 128}
236-
y={currentPosition.y - 128}
237-
fill='url(#clerk-checkout-success-gradient)'
238-
filter='url(#clerk-checkout-success-blur-effect)'
239-
/>
240-
)}
241-
</g>
242-
</Box>
252+
<SuccessRing
253+
positionX={mousePosition.x}
254+
positionY={mousePosition.y}
255+
/>
243256
<Box
244257
elementDescriptor={descriptors.checkoutSuccessBadge}
245258
sx={t => ({

0 commit comments

Comments
 (0)