Skip to content

Commit ec3020b

Browse files
committed
Optimize
1 parent 06f819c commit ec3020b

File tree

9 files changed

+116
-23
lines changed

9 files changed

+116
-23
lines changed

src/code-impl.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { resetSelectorPropsCache } from './codegen/props/selector'
44
import { ResponsiveCodegen } from './codegen/responsive/ResponsiveCodegen'
55
import { nodeProxyTracker } from './codegen/utils/node-proxy'
66
import { perfEnd, perfReport, perfReset, perfStart } from './codegen/utils/perf'
7+
import { resetVariableCache } from './codegen/utils/variable-cache'
78
import { wrapComponent } from './codegen/utils/wrap-component'
89
import { exportDevup, importDevup } from './commands/devup'
910
import { exportAssets } from './commands/exportAssets'
1011
import { exportComponents } from './commands/exportComponents'
1112
import { exportPagesAndComponents } from './commands/exportPagesAndComponents'
12-
import { getComponentName } from './utils'
13+
import { getComponentName, resetTextStyleCache } from './utils'
1314
import { toPascal } from './utils/to-pascal'
1415

1516
const DEVUP_COMPONENTS = [
@@ -132,6 +133,8 @@ export function registerCodegen(ctx: typeof figma) {
132133
perfReset()
133134
resetGetPropsCache()
134135
resetSelectorPropsCache()
136+
resetVariableCache()
137+
resetTextStyleCache()
135138

136139
let t = perfStart()
137140
const codegen = new Codegen(node)

src/codegen/Codegen.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,11 @@ export class Codegen {
212212

213213
// Build children sequentially to avoid Figma API contention.
214214
// getProps(node) is already in-flight concurrently above.
215+
// INSTANCE children are handled inside doBuildTree when buildTree(child) recurses —
216+
// no pre-call to getMainComponentAsync needed (was causing duplicate Figma API calls).
215217
const children: NodeTree[] = []
216218
if ('children' in node) {
217219
for (const child of node.children) {
218-
if (child.type === 'INSTANCE') {
219-
const mainComponent = await child.getMainComponentAsync()
220-
if (mainComponent) await this.addComponentTree(mainComponent)
221-
}
222220
children.push(await this.buildTree(child))
223221
}
224222
}
@@ -296,14 +294,11 @@ export class Codegen {
296294
const t = perfStart()
297295
const selectorPropsPromise = getSelectorProps(node)
298296

299-
// Build children sequentially to avoid Figma API contention
297+
// Build children sequentially to avoid Figma API contention.
298+
// INSTANCE children are handled inside doBuildTree when buildTree(child) recurses.
300299
const childrenTrees: NodeTree[] = []
301300
if ('children' in node) {
302301
for (const child of node.children) {
303-
if (child.type === 'INSTANCE') {
304-
const mainComponent = await child.getMainComponentAsync()
305-
if (mainComponent) await this.addComponentTree(mainComponent)
306-
}
307302
childrenTrees.push(await this.buildTree(child))
308303
}
309304
}

src/codegen/props/index.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ export async function getProps(
5151
// Fire all async prop getters in parallel — they are independent
5252
// (no shared mutable state, no inter-function data dependencies).
5353
// Skip TEXT-only async getters for non-TEXT nodes.
54+
const tBorder = perfStart()
55+
const borderP = getBorderProps(node)
56+
const tBg = perfStart()
57+
const bgP = getBackgroundProps(node)
58+
const tEffect = perfStart()
59+
const effectP = getEffectProps(node)
60+
const tTextStroke = perfStart()
61+
const textStrokeP = isText ? getTextStrokeProps(node) : undefined
62+
const tTextShadow = perfStart()
63+
const textShadowP = isText ? getTextShadowProps(node) : undefined
64+
const tReaction = perfStart()
65+
const reactionP = getReactionProps(node)
66+
5467
const [
5568
borderProps,
5669
backgroundProps,
@@ -59,12 +72,34 @@ export async function getProps(
5972
textShadowProps,
6073
reactionProps,
6174
] = await Promise.all([
62-
getBorderProps(node),
63-
getBackgroundProps(node),
64-
getEffectProps(node),
65-
isText ? getTextStrokeProps(node) : undefined,
66-
isText ? getTextShadowProps(node) : undefined,
67-
getReactionProps(node),
75+
borderP.then((r) => {
76+
perfEnd('getProps.border', tBorder)
77+
return r
78+
}),
79+
bgP.then((r) => {
80+
perfEnd('getProps.background', tBg)
81+
return r
82+
}),
83+
effectP.then((r) => {
84+
perfEnd('getProps.effect', tEffect)
85+
return r
86+
}),
87+
textStrokeP
88+
? textStrokeP.then((r) => {
89+
perfEnd('getProps.textStroke', tTextStroke)
90+
return r
91+
})
92+
: undefined,
93+
textShadowP
94+
? textShadowP.then((r) => {
95+
perfEnd('getProps.textShadow', tTextShadow)
96+
return r
97+
})
98+
: undefined,
99+
reactionP.then((r) => {
100+
perfEnd('getProps.reaction', tReaction)
101+
return r
102+
}),
68103
])
69104

70105
return {

src/codegen/render/text.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { propsToPropsWithTypography } from '../../utils'
22
import { textSegmentToTypography } from '../../utils/text-segment-to-typography'
33
import { fixTextChild } from '../utils/fix-text-child'
44
import { paintToCSS } from '../utils/paint-to-css'
5+
import { perfEnd, perfStart } from '../utils/perf'
56
import { renderNode } from '.'
67

78
/**
@@ -34,6 +35,7 @@ export async function renderText(node: TextNode): Promise<{
3435
children: string[]
3536
props: Record<string, string>
3637
}> {
38+
const tRender = perfStart()
3739
const segs = node.getStyledTextSegments(SEGMENT_TYPE)
3840

3941
// select main color
@@ -139,15 +141,18 @@ export async function renderText(node: TextNode): Promise<{
139141
)
140142
const resultChildren = children.flat()
141143

142-
if (resultChildren.length === 1)
144+
if (resultChildren.length === 1) {
145+
perfEnd('renderText()', tRender)
143146
return {
144147
children: resultChildren[0].children,
145148
props: {
146149
...defaultProps,
147150
...resultChildren[0].props,
148151
},
149152
}
153+
}
150154

155+
perfEnd('renderText()', tRender)
151156
return {
152157
children: resultChildren.map((child) => {
153158
if (Object.keys(child.props).length === 0)

src/codegen/utils/__tests__/paint-to-css.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { describe, expect, mock, test } from 'bun:test'
1+
import { beforeEach, describe, expect, mock, test } from 'bun:test'
22
import { paintToCSS } from '../paint-to-css'
3+
import { resetVariableCache } from '../variable-cache'
34

45
// mock asset checker to avoid real node handling
56
mock.module('../check-asset-node', () => ({
67
checkAssetNode: () => 'png',
78
}))
89

910
describe('paintToCSS', () => {
11+
beforeEach(() => {
12+
resetVariableCache()
13+
})
1014
test('converts image paint with TILE scaleMode to repeat url', async () => {
1115
;(globalThis as { figma?: unknown }).figma = {
1216
util: { rgba: (v: unknown) => v },

src/codegen/utils/paint-to-css.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { toCamel } from '../../utils/to-camel'
44
import { checkAssetNode } from './check-asset-node'
55
import { fmtPct } from './fmtPct'
66
import { solidToString } from './solid-to-string'
7+
import { getVariableByIdCached } from './variable-cache'
78
import { buildCssUrl } from './wrap-url'
89

910
interface Point {
@@ -19,7 +20,7 @@ async function processGradientStopColor(
1920
opacity: number,
2021
): Promise<string> {
2122
if (stop.boundVariables?.color) {
22-
const variable = await figma.variables.getVariableByIdAsync(
23+
const variable = await getVariableByIdCached(
2324
stop.boundVariables.color.id as string,
2425
)
2526
if (variable?.name) {

src/codegen/utils/solid-to-string.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { optimizeHex } from '../../utils/optimize-hex'
22
import { rgbaToHex } from '../../utils/rgba-to-hex'
33
import { toCamel } from '../../utils/to-camel'
4+
import { getVariableByIdCached } from './variable-cache'
45

56
export async function solidToString(solid: SolidPaint) {
67
if (solid.boundVariables?.color) {
7-
const variable = await figma.variables.getVariableByIdAsync(
8+
const variable = await getVariableByIdCached(
89
solid.boundVariables.color.id as string,
910
)
1011
if (variable?.name) return `$${toCamel(variable.name)}`
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Global cache for figma.variables.getVariableByIdAsync() results.
2+
// This is the single hottest Figma API call — used by solidToString,
3+
// processGradientStopColor, and transitively by every prop getter that
4+
// resolves colors (border, background, text-stroke, reaction, renderText).
5+
// Keyed by variable ID; stores the Promise to deduplicate concurrent calls.
6+
const variableByIdCache = new Map<string, Promise<Variable | null>>()
7+
8+
export function getVariableByIdCached(
9+
variableId: string,
10+
): Promise<Variable | null> {
11+
const cached = variableByIdCache.get(variableId)
12+
if (cached) return cached
13+
const promise = Promise.resolve(
14+
figma.variables.getVariableByIdAsync(variableId),
15+
)
16+
variableByIdCache.set(variableId, promise)
17+
return promise
18+
}
19+
20+
export function resetVariableCache(): void {
21+
variableByIdCache.clear()
22+
}

src/utils.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,43 @@
11
import { toCamel } from './utils/to-camel'
22
import { toPascal } from './utils/to-pascal'
33

4+
// Cache for figma.getLocalTextStylesAsync() — called once per codegen run
5+
let localTextStyleIdsCache: Promise<Set<string>> | null = null
6+
7+
function getLocalTextStyleIds(): Promise<Set<string>> {
8+
if (localTextStyleIdsCache) return localTextStyleIdsCache
9+
localTextStyleIdsCache = Promise.resolve(
10+
figma.getLocalTextStylesAsync(),
11+
).then((styles) => new Set(styles.map((s) => s.id)))
12+
return localTextStyleIdsCache
13+
}
14+
15+
// Cache for figma.getStyleByIdAsync() — keyed by style ID
16+
const styleByIdCache = new Map<string, Promise<BaseStyle | null>>()
17+
18+
function getStyleByIdCached(styleId: string): Promise<BaseStyle | null> {
19+
const cached = styleByIdCache.get(styleId)
20+
if (cached) return cached
21+
const promise = Promise.resolve(figma.getStyleByIdAsync(styleId))
22+
styleByIdCache.set(styleId, promise)
23+
return promise
24+
}
25+
26+
export function resetTextStyleCache(): void {
27+
localTextStyleIdsCache = null
28+
styleByIdCache.clear()
29+
}
30+
431
export async function propsToPropsWithTypography(
532
props: Record<string, unknown>,
633
textStyleId: string,
734
) {
835
const ret: Record<string, unknown> = { ...props }
936
delete ret.w
1037
delete ret.h
11-
const styles = await figma.getLocalTextStylesAsync()
12-
if (textStyleId && styles.find((style) => style.id === textStyleId)) {
13-
const style = await figma.getStyleByIdAsync(textStyleId)
38+
const localStyleIds = await getLocalTextStyleIds()
39+
if (textStyleId && localStyleIds.has(textStyleId)) {
40+
const style = await getStyleByIdCached(textStyleId)
1441
if (style) {
1542
const split = style.name.split('/')
1643
ret.typography = toCamel(split[split.length - 1])

0 commit comments

Comments
 (0)