Skip to content

Commit fce9a20

Browse files
committed
feat: rewrite converter for significant perf increase
1 parent ca4e913 commit fce9a20

File tree

2 files changed

+391
-43
lines changed

2 files changed

+391
-43
lines changed

src/__tests__/converter.test.ts

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
import React from 'react'
2+
3+
import type { AbstractElement } from '@fortawesome/fontawesome-svg-core'
4+
5+
import { convert, styleCache } from '../converter'
6+
7+
// Mock data structures for testing
8+
const createMockElement = (
9+
tag: string = 'svg',
10+
attributes: Record<string, unknown> = {},
11+
children: AbstractElement[] = [],
12+
): AbstractElement => ({
13+
tag,
14+
attributes,
15+
children,
16+
})
17+
18+
const COMPLEX_STYLE = [
19+
'fill: currentColor',
20+
'display: inline-block',
21+
'font-size: inherit',
22+
'height: 1em',
23+
'overflow: visible',
24+
'vertical-align: -0.125em',
25+
'width: 1em',
26+
'-webkit-font-smoothing: antialiased',
27+
'-moz-osx-font-smoothing: grayscale',
28+
].join('; ')
29+
30+
const SIMPLE_STYLE = 'fill: currentColor; display: inline-block;'
31+
32+
describe('convert function performance', () => {
33+
let mockCreateElement: jest.MockedFunction<typeof React.createElement>
34+
35+
beforeEach(() => {
36+
mockCreateElement = jest
37+
.fn()
38+
.mockImplementation(
39+
(tag: string, props: object, ...children: object[]) => ({
40+
type: tag,
41+
props: { ...props, children },
42+
}),
43+
)
44+
})
45+
46+
afterEach(() => {
47+
jest.clearAllMocks()
48+
})
49+
50+
describe('baseline functionality', () => {
51+
it('should handle simple string elements', () => {
52+
const result = convert(
53+
mockCreateElement,
54+
'text' as unknown as AbstractElement,
55+
)
56+
expect(result).toBe('text')
57+
})
58+
59+
it('should handle elements with basic attributes', () => {
60+
const element = createMockElement('svg', {
61+
class: 'fa-icon',
62+
style: 'fill: red',
63+
})
64+
65+
convert(mockCreateElement, element)
66+
67+
expect(mockCreateElement).toHaveBeenCalledWith('svg', {
68+
className: 'fa-icon',
69+
style: { fill: 'red' },
70+
})
71+
})
72+
73+
it('should handle nested elements', () => {
74+
const child = createMockElement('path', { d: 'M0,0 L10,10' })
75+
const parent = createMockElement('svg', { class: 'fa-icon' }, [child])
76+
77+
convert(mockCreateElement, parent)
78+
79+
expect(mockCreateElement).toHaveBeenCalledTimes(2)
80+
})
81+
})
82+
83+
describe('style parsing performance', () => {
84+
it('should parse complex styles efficiently', () => {
85+
const element = createMockElement('svg', { style: COMPLEX_STYLE })
86+
87+
const startTime = performance.now()
88+
89+
// Run multiple times to measure consistent performance
90+
for (let i = 0; i < 100; i++) {
91+
convert(mockCreateElement, element)
92+
}
93+
94+
const endTime = performance.now()
95+
const duration = endTime - startTime
96+
97+
// Expect reasonable performance (less than 50ms for 100 iterations)
98+
expect(duration).toBeLessThan(50)
99+
})
100+
101+
it('should benefit from style caching', () => {
102+
const element = createMockElement('svg', { style: COMPLEX_STYLE })
103+
104+
const numOfIterations = 1000
105+
let firstRunDuration = 0
106+
let secondRunDuration = 0
107+
108+
// Run 1000 times to test average performance
109+
for (let i = 0; i < numOfIterations; i++) {
110+
// First run (no cache)
111+
const firstRunStart = performance.now()
112+
convert(mockCreateElement, element)
113+
114+
const firstRunEnd = performance.now()
115+
firstRunDuration += firstRunEnd - firstRunStart
116+
117+
// Second run (with cache)
118+
const secondRunStart = performance.now()
119+
120+
convert(mockCreateElement, element)
121+
const secondRunEnd = performance.now()
122+
secondRunDuration += secondRunEnd - secondRunStart
123+
124+
styleCache.clear() // Clear cache to ensure fresh runs
125+
}
126+
127+
const firstRunAverage = firstRunDuration / numOfIterations
128+
const secondRunAverage = secondRunDuration / numOfIterations
129+
130+
// Second run should be significantly faster (at least 30% improvement)
131+
expect(secondRunAverage).toBeLessThan(firstRunAverage * 0.7)
132+
})
133+
134+
it('should handle many unique styles without memory issues', () => {
135+
const startMemory = process.memoryUsage().heapUsed
136+
137+
// Create 2000 unique styles to test cache management
138+
for (let i = 0; i < 2000; i++) {
139+
const uniqueStyle = `fill: rgb(${i}, ${i % 255}, ${(i * 2) % 255}); opacity: ${i / 2000}`
140+
const element = createMockElement('svg', { style: uniqueStyle })
141+
convert(mockCreateElement, element)
142+
}
143+
144+
const endMemory = process.memoryUsage().heapUsed
145+
const memoryIncrease = endMemory - startMemory
146+
147+
// Memory increase should be reasonable (less than 10MB)
148+
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024)
149+
})
150+
})
151+
152+
describe('attribute processing performance', () => {
153+
it('should handle many attributes efficiently', () => {
154+
const manyAttributes: Record<string, unknown> = {}
155+
156+
// Create 50 attributes of various types
157+
for (let i = 0; i < 50; i++) {
158+
manyAttributes[`data-attr-${i}`] = `value-${i}`
159+
manyAttributes[`aria-label-${i}`] = `label-${i}`
160+
}
161+
manyAttributes.class = 'fa-icon fa-large'
162+
manyAttributes.style = COMPLEX_STYLE
163+
164+
const element = createMockElement('svg', manyAttributes)
165+
166+
const startTime = performance.now()
167+
168+
for (let i = 0; i < 100; i++) {
169+
convert(mockCreateElement, element)
170+
}
171+
172+
const endTime = performance.now()
173+
const duration = endTime - startTime
174+
175+
// Should handle many attributes efficiently
176+
expect(duration).toBeLessThan(100)
177+
})
178+
179+
it('should process aria and data attributes correctly', () => {
180+
const element = createMockElement('svg', {
181+
'aria-hidden': 'true',
182+
'aria-label': 'test icon',
183+
'data-testid': 'icon',
184+
'data-custom': 'value',
185+
})
186+
187+
const startTime = performance.now()
188+
convert(mockCreateElement, element)
189+
const endTime = performance.now()
190+
191+
expect(endTime - startTime).toBeLessThan(5) // Should be very fast
192+
expect(mockCreateElement).toHaveBeenCalledWith(
193+
'svg',
194+
expect.objectContaining({
195+
'aria-label': 'test icon',
196+
'aria-hidden': 'false', // Should be overridden
197+
'data-testid': 'icon',
198+
'data-custom': 'value',
199+
}),
200+
)
201+
})
202+
})
203+
204+
describe('memory efficiency', () => {
205+
it('should not create excessive temporary objects', () => {
206+
const initialMemory = process.memoryUsage().heapUsed
207+
208+
// Process many elements to test memory efficiency
209+
for (let i = 0; i < 1000; i++) {
210+
const element = createMockElement('svg', {
211+
class: `fa-icon-${i}`,
212+
style: i % 10 === 0 ? COMPLEX_STYLE : SIMPLE_STYLE,
213+
'data-index': i.toString(),
214+
})
215+
convert(mockCreateElement, element)
216+
}
217+
218+
// Force garbage collection if available
219+
if (globalThis.gc) {
220+
globalThis.gc()
221+
}
222+
223+
const finalMemory = process.memoryUsage().heapUsed
224+
const memoryIncrease = finalMemory - initialMemory
225+
226+
// Memory increase should be minimal (less than 2.5MB)
227+
expect(memoryIncrease).toBeLessThan(2.5 * 1024 * 1024)
228+
})
229+
})
230+
231+
describe('real-world performance scenarios', () => {
232+
it('should handle typical FontAwesome icon conversion efficiently', () => {
233+
// Simulate a typical FontAwesome SVG structure
234+
const iconElement = createMockElement(
235+
'svg',
236+
{
237+
'aria-hidden': 'true',
238+
focusable: 'false',
239+
'data-prefix': 'fas',
240+
'data-icon': 'coffee',
241+
class: 'svg-inline--fa fa-coffee',
242+
role: 'img',
243+
style:
244+
'fill: currentColor; display: inline-block; font-size: inherit; height: 1em; overflow: visible; vertical-align: -0.125em;',
245+
viewBox: '0 0 640 512',
246+
},
247+
[
248+
createMockElement('path', {
249+
fill: 'currentColor',
250+
d: 'M192 384h192c53 0 96-43 96-96h32c70.6 0 128-57.4 128-128S582.6 32 512 32H120c-13.3 0-24 10.7-24 24v232c0 53 43 96 96 96z',
251+
}),
252+
],
253+
)
254+
255+
const extraProps = {
256+
className: 'custom-icon',
257+
style: { color: '#007bff' },
258+
onClick: jest.fn(),
259+
}
260+
261+
const startTime = performance.now()
262+
263+
// Simulate rendering 100 icons (typical for a page with many icons)
264+
for (let i = 0; i < 100; i++) {
265+
convert(mockCreateElement, iconElement, extraProps)
266+
}
267+
268+
const endTime = performance.now()
269+
const duration = endTime - startTime
270+
271+
// Should handle 100 typical icons very efficiently
272+
expect(duration).toBeLessThan(25)
273+
})
274+
275+
it('should handle icon list rendering efficiently', () => {
276+
// Simulate rendering a list of different icons
277+
const iconConfigs = [
278+
{ class: 'fa-coffee', style: 'color: brown' },
279+
{ class: 'fa-heart', style: 'color: red' },
280+
{ class: 'fa-star', style: 'color: gold' },
281+
{ class: 'fa-user', style: 'color: blue' },
282+
{ class: 'fa-home', style: 'color: green' },
283+
]
284+
285+
const startTime = performance.now()
286+
287+
// Render each icon type 20 times (100 total icons)
288+
for (const [index, config] of iconConfigs.entries()) {
289+
for (let i = 0; i < 20; i++) {
290+
const element = createMockElement(
291+
'svg',
292+
{
293+
class: `svg-inline--fa ${config.class}`,
294+
style: config.style,
295+
'data-icon': config.class.replace('fa-', ''),
296+
},
297+
[
298+
createMockElement('path', {
299+
fill: 'currentColor',
300+
d: `path-data-${index}`,
301+
}),
302+
],
303+
)
304+
305+
convert(mockCreateElement, element, {
306+
key: `${config.class}-${i}`,
307+
})
308+
}
309+
}
310+
311+
const endTime = performance.now()
312+
const duration = endTime - startTime
313+
314+
// Should handle rendering 100 varied icons efficiently
315+
expect(duration).toBeLessThan(30)
316+
})
317+
})
318+
})

0 commit comments

Comments
 (0)