Skip to content

Commit 03cad70

Browse files
committed
feat: optimise FontAwesomeIcon by reducing per-render function calls and memory allocations
1 parent 3ed3ade commit 03cad70

File tree

5 files changed

+37
-83
lines changed

5 files changed

+37
-83
lines changed

src/components/FontAwesomeIcon.tsx

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { CSSProperties, RefAttributes, SVGAttributes } from 'react'
22

3-
import { icon, parse } from '@fortawesome/fontawesome-svg-core'
3+
import {
4+
icon as faIcon,
5+
parse as faParse,
6+
} from '@fortawesome/fontawesome-svg-core'
47
import type {
58
FaSymbol,
69
FlipProp,
@@ -15,7 +18,6 @@ import { convert } from '../converter'
1518
import { Logger } from '../logger'
1619
import { getClassListFromProps } from '../utils/get-class-list-from-props'
1720
import { normalizeIconArgs } from '../utils/normalize-icon-args'
18-
import { objectWithKey } from '../utils/object-with-key'
1921
import { typedObjectKeys } from '../utils/typed-object-keys'
2022

2123
const logger = new Logger('FontAwesomeIcon')
@@ -51,6 +53,8 @@ const defaultProps = {
5153
widthAuto: false,
5254
}
5355

56+
const EMPTY_OBJECT: Record<string, never> = {}
57+
5458
export interface FontAwesomeIconProps
5559
extends Omit<SVGAttributes<SVGSVGElement>, 'children' | 'mask' | 'transform'>,
5660
RefAttributes<SVGSVGElement> {
@@ -155,35 +159,30 @@ export const FontAwesomeIcon = React.forwardRef(
155159
icon: iconArgs,
156160
mask: maskArgs,
157161
symbol,
158-
className,
159162
title,
160163
titleId,
161164
maskId,
165+
transform,
162166
} = allProps
163167

164168
const iconLookup = normalizeIconArgs(iconArgs)
165169

166-
const classes = objectWithKey('classes', [
167-
...getClassListFromProps(allProps),
168-
...(className || '').split(' '),
169-
])
170-
const transform = objectWithKey(
171-
'transform',
172-
typeof allProps.transform === 'string'
173-
? parse.transform(allProps.transform)
174-
: allProps.transform,
175-
)
176-
const mask = objectWithKey('mask', normalizeIconArgs(maskArgs))
177-
178170
if (!iconLookup) {
179171
logger.error('Icon lookup is undefined', iconArgs)
180172
return null
181173
}
182174

183-
const renderedIcon = icon(iconLookup, {
184-
...classes,
185-
...transform,
186-
...mask,
175+
const classList = getClassListFromProps(allProps)
176+
177+
const transformProps =
178+
typeof transform === 'string' ? faParse.transform(transform) : transform
179+
180+
const normalizedMaskArgs = normalizeIconArgs(maskArgs)
181+
182+
const renderedIcon = faIcon(iconLookup, {
183+
...(classList.length > 0 ? { classes: classList } : EMPTY_OBJECT),
184+
...(transformProps ? { transform: transformProps } : EMPTY_OBJECT),
185+
...(normalizedMaskArgs ? { mask: normalizedMaskArgs } : EMPTY_OBJECT),
187186
symbol,
188187
title,
189188
titleId,

src/utils/__tests__/get-class-list-from-props.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('get class list', () => {
3131
flip: true,
3232
rotateBy: true,
3333
widthAuto: true,
34-
}
34+
} as FontAwesomeIconProps
3535

3636
const classList = getClassListFromProps(props)
3737
const expectedClasses = [
@@ -78,7 +78,9 @@ describe('get class list', () => {
7878
'9x',
7979
'10x',
8080
])('size %s', (size) => {
81-
expect(getClassListFromProps({ size })).toStrictEqual([`fa-${size}`])
81+
expect(
82+
getClassListFromProps({ size } as FontAwesomeIconProps),
83+
).toStrictEqual([`fa-${size}`])
8284
})
8385

8486
test('flip', () => {
@@ -88,19 +90,19 @@ describe('get class list', () => {
8890

8991
const horizontalList = getClassListFromProps({
9092
flip: 'horizontal',
91-
})
93+
} as FontAwesomeIconProps)
9294

9395
const verticalList = getClassListFromProps({
9496
flip: 'vertical',
95-
})
97+
} as FontAwesomeIconProps)
9698

9799
const bothList = getClassListFromProps({
98100
flip: 'both',
99-
})
101+
} as FontAwesomeIconProps)
100102

101103
const flipAnimationOnly = getClassListFromProps({
102104
flip: true,
103-
})
105+
} as FontAwesomeIconProps)
104106

105107
expect(horizontalList).toContain(HORIZONTAL)
106108
expect(verticalList).toContain(VERTICAL)
@@ -113,13 +115,15 @@ describe('get class list', () => {
113115
})
114116

115117
test.each<RotateProp>([90, 180, 270])('rotation %s', (rotation) => {
116-
expect(getClassListFromProps({ rotation })).toStrictEqual([
117-
`fa-rotate-${rotation}`,
118-
])
118+
expect(
119+
getClassListFromProps({ rotation } as FontAwesomeIconProps),
120+
).toStrictEqual([`fa-rotate-${rotation}`])
119121
})
120122

121123
test.each<PullProp>(['left', 'right'])('pull %s', (pull) => {
122-
expect(getClassListFromProps({ pull })).toStrictEqual([`fa-pull-${pull}`])
124+
expect(
125+
getClassListFromProps({ pull } as FontAwesomeIconProps),
126+
).toStrictEqual([`fa-pull-${pull}`])
123127
})
124128

125129
test.each<keyof FontAwesomeIconProps>(['pull', 'rotation', 'size'])(
@@ -135,7 +139,7 @@ describe('get class list', () => {
135139
border: true,
136140
listItem: true,
137141
[prop]: null,
138-
}
142+
} as unknown as FontAwesomeIconProps
139143

140144
expect(getClassListFromProps(props).length).toBe(NUM_CLASSES)
141145
expect(getClassListFromProps(props)).toStrictEqual([

src/utils/__tests__/object-with-key.test.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/utils/get-class-list-from-props.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@ const IS_VERSION_7_OR_LATER = semver.gte(
2626
* Get CSS class list from a props object.
2727
* This function maps our React props to the corresponding CSS class names from Font Awesome.
2828
*
29-
* @param props Partial props object from which to extract CSS classes.
29+
* @param props Props object from which to extract CSS classes.
3030
* @returns An array of CSS class names derived from the props.
3131
*/
32-
export function getClassListFromProps(
33-
props: Partial<FontAwesomeIconProps>,
34-
): string[] {
32+
export function getClassListFromProps(props: FontAwesomeIconProps): string[] {
3533
const {
3634
beat,
3735
fade,
@@ -53,11 +51,13 @@ export function getClassListFromProps(
5351
swapOpacity,
5452
rotateBy,
5553
widthAuto,
54+
className,
5655
} = props
5756

5857
const result: string[] = []
5958

6059
// Add classes only if the condition is truthy
60+
if (className) result.push(...className.split(' '))
6161
if (beat) result.push(ANIMATION_CLASSES.beat)
6262
if (fade) result.push(ANIMATION_CLASSES.fade)
6363
if (beatFade) result.push(ANIMATION_CLASSES.beatFade)

src/utils/object-with-key.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)