Skip to content

Commit 0a4e55f

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

File tree

2 files changed

+425
-43
lines changed

2 files changed

+425
-43
lines changed

src/__tests__/converter.test.ts

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
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 50% improvement)
131+
expect(secondRunAverage).toBeLessThan(firstRunAverage * 0.5)
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+
it('should parse styles faster than baseline approach', () => {
152+
const element = createMockElement('svg', { style: COMPLEX_STYLE })
153+
154+
// Our optimized approach
155+
const optimizedStart = performance.now()
156+
for (let i = 0; i < 1000; i++) {
157+
convert(mockCreateElement, element)
158+
}
159+
const optimizedEnd = performance.now()
160+
const optimizedDuration = optimizedEnd - optimizedStart
161+
162+
// Baseline approach (simulating old split/map/filter)
163+
const baselineStart = performance.now()
164+
for (let i = 0; i < 1000; i++) {
165+
// Simulate the old approach
166+
const pairs = COMPLEX_STYLE.split(';')
167+
.map((s) => s.trim())
168+
.filter(Boolean)
169+
170+
for (const pair of pairs) {
171+
const colonIndex = pair.indexOf(':')
172+
if (colonIndex > 0) {
173+
pair.slice(0, colonIndex).trim()
174+
pair.slice(colonIndex + 1).trim()
175+
}
176+
}
177+
}
178+
const baselineEnd = performance.now()
179+
const baselineDuration = baselineEnd - baselineStart
180+
181+
// Our approach should be faster (allow some variance for test stability)
182+
expect(optimizedDuration).toBeLessThan(baselineDuration * 1.2)
183+
})
184+
})
185+
186+
describe('attribute processing performance', () => {
187+
it('should handle many attributes efficiently', () => {
188+
const manyAttributes: Record<string, unknown> = {}
189+
190+
// Create 50 attributes of various types
191+
for (let i = 0; i < 50; i++) {
192+
manyAttributes[`data-attr-${i}`] = `value-${i}`
193+
manyAttributes[`aria-label-${i}`] = `label-${i}`
194+
}
195+
manyAttributes.class = 'fa-icon fa-large'
196+
manyAttributes.style = COMPLEX_STYLE
197+
198+
const element = createMockElement('svg', manyAttributes)
199+
200+
const startTime = performance.now()
201+
202+
for (let i = 0; i < 100; i++) {
203+
convert(mockCreateElement, element)
204+
}
205+
206+
const endTime = performance.now()
207+
const duration = endTime - startTime
208+
209+
// Should handle many attributes efficiently
210+
expect(duration).toBeLessThan(100)
211+
})
212+
213+
it('should process aria and data attributes correctly', () => {
214+
const element = createMockElement('svg', {
215+
'aria-hidden': 'true',
216+
'aria-label': 'test icon',
217+
'data-testid': 'icon',
218+
'data-custom': 'value',
219+
})
220+
221+
const startTime = performance.now()
222+
convert(mockCreateElement, element)
223+
const endTime = performance.now()
224+
225+
expect(endTime - startTime).toBeLessThan(5) // Should be very fast
226+
expect(mockCreateElement).toHaveBeenCalledWith(
227+
'svg',
228+
expect.objectContaining({
229+
'aria-label': 'test icon',
230+
'aria-hidden': 'false', // Should be overridden
231+
'data-testid': 'icon',
232+
'data-custom': 'value',
233+
}),
234+
)
235+
})
236+
})
237+
238+
describe('memory efficiency', () => {
239+
it('should not create excessive temporary objects', () => {
240+
const initialMemory = process.memoryUsage().heapUsed
241+
242+
// Process many elements to test memory efficiency
243+
for (let i = 0; i < 1000; i++) {
244+
const element = createMockElement('svg', {
245+
class: `fa-icon-${i}`,
246+
style: i % 10 === 0 ? COMPLEX_STYLE : SIMPLE_STYLE,
247+
'data-index': i.toString(),
248+
})
249+
convert(mockCreateElement, element)
250+
}
251+
252+
// Force garbage collection if available
253+
if (globalThis.gc) {
254+
globalThis.gc()
255+
}
256+
257+
const finalMemory = process.memoryUsage().heapUsed
258+
const memoryIncrease = finalMemory - initialMemory
259+
260+
// Memory increase should be minimal (less than 2.5MB)
261+
expect(memoryIncrease).toBeLessThan(2.5 * 1024 * 1024)
262+
})
263+
})
264+
265+
describe('real-world performance scenarios', () => {
266+
it('should handle typical FontAwesome icon conversion efficiently', () => {
267+
// Simulate a typical FontAwesome SVG structure
268+
const iconElement = createMockElement(
269+
'svg',
270+
{
271+
'aria-hidden': 'true',
272+
focusable: 'false',
273+
'data-prefix': 'fas',
274+
'data-icon': 'coffee',
275+
class: 'svg-inline--fa fa-coffee',
276+
role: 'img',
277+
style:
278+
'fill: currentColor; display: inline-block; font-size: inherit; height: 1em; overflow: visible; vertical-align: -0.125em;',
279+
viewBox: '0 0 640 512',
280+
},
281+
[
282+
createMockElement('path', {
283+
fill: 'currentColor',
284+
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',
285+
}),
286+
],
287+
)
288+
289+
const extraProps = {
290+
className: 'custom-icon',
291+
style: { color: '#007bff' },
292+
onClick: jest.fn(),
293+
}
294+
295+
const startTime = performance.now()
296+
297+
// Simulate rendering 100 icons (typical for a page with many icons)
298+
for (let i = 0; i < 100; i++) {
299+
convert(mockCreateElement, iconElement, extraProps)
300+
}
301+
302+
const endTime = performance.now()
303+
const duration = endTime - startTime
304+
305+
// Should handle 100 typical icons very efficiently
306+
expect(duration).toBeLessThan(25)
307+
})
308+
309+
it('should handle icon list rendering efficiently', () => {
310+
// Simulate rendering a list of different icons
311+
const iconConfigs = [
312+
{ class: 'fa-coffee', style: 'color: brown' },
313+
{ class: 'fa-heart', style: 'color: red' },
314+
{ class: 'fa-star', style: 'color: gold' },
315+
{ class: 'fa-user', style: 'color: blue' },
316+
{ class: 'fa-home', style: 'color: green' },
317+
]
318+
319+
const startTime = performance.now()
320+
321+
// Render each icon type 20 times (100 total icons)
322+
for (const [index, config] of iconConfigs.entries()) {
323+
for (let i = 0; i < 20; i++) {
324+
const element = createMockElement(
325+
'svg',
326+
{
327+
class: `svg-inline--fa ${config.class}`,
328+
style: config.style,
329+
'data-icon': config.class.replace('fa-', ''),
330+
},
331+
[
332+
createMockElement('path', {
333+
fill: 'currentColor',
334+
d: `path-data-${index}`,
335+
}),
336+
],
337+
)
338+
339+
convert(mockCreateElement, element, {
340+
key: `${config.class}-${i}`,
341+
})
342+
}
343+
}
344+
345+
const endTime = performance.now()
346+
const duration = endTime - startTime
347+
348+
// Should handle rendering 100 varied icons efficiently
349+
expect(duration).toBeLessThan(30)
350+
})
351+
})
352+
})

0 commit comments

Comments
 (0)