Skip to content

Commit 95a46f7

Browse files
committed
Optimize
1 parent 8c6aeef commit 95a46f7

File tree

1 file changed

+81
-68
lines changed

1 file changed

+81
-68
lines changed

src/codegen/Codegen.ts

Lines changed: 81 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { getComponentName } from '../utils'
22
import { getProps } from './props'
3+
import { getPositionProps } from './props/position'
34
import { getSelectorProps } from './props/selector'
5+
import { getTransformProps } from './props/transform'
46
import { renderComponent, renderNode } from './render'
57
import { renderText } from './render/text'
68
import type { ComponentTree, NodeTree } from './types'
@@ -25,6 +27,9 @@ export class Codegen {
2527
// Tree representations
2628
private tree: NodeTree | null = null
2729
private componentTrees: Map<string, ComponentTree> = new Map()
30+
// Cache buildTree results by node.id to avoid duplicate subtree builds
31+
// (e.g., when addComponentTree and main tree walk process the same children)
32+
private buildTreeCache: Map<string, Promise<NodeTree>> = new Map()
2833

2934
constructor(private node: SceneNode) {
3035
this.node = node
@@ -96,6 +101,19 @@ export class Codegen {
96101
* This is the intermediate JSON representation that can be compared/merged.
97102
*/
98103
async buildTree(node: SceneNode = this.node): Promise<NodeTree> {
104+
const cacheKey = node.id
105+
if (cacheKey) {
106+
const cached = this.buildTreeCache.get(cacheKey)
107+
if (cached) return cached
108+
}
109+
const promise = this.doBuildTree(node)
110+
if (cacheKey) {
111+
this.buildTreeCache.set(cacheKey, promise)
112+
}
113+
return promise
114+
}
115+
116+
private async doBuildTree(node: SceneNode): Promise<NodeTree> {
99117
const tBuild = perfStart()
100118
// Handle asset nodes (images/SVGs)
101119
const assetNode = checkAssetNode(node)
@@ -122,46 +140,29 @@ export class Codegen {
122140
}
123141
}
124142

125-
// Run getProps and component tree registration in parallel with children building.
126-
// These are independent: props depend only on this node, children depend on child nodes.
127-
const propsPromise = getProps(node)
128-
129-
// Handle COMPONENT_SET or COMPONENT - add to componentTrees
130-
const componentTreePromise =
131-
(node.type === 'COMPONENT_SET' || node.type === 'COMPONENT') &&
132-
((this.node.type === 'COMPONENT_SET' &&
133-
node === this.node.defaultVariant) ||
134-
this.node.type === 'COMPONENT')
135-
? this.addComponentTree(
136-
node.type === 'COMPONENT_SET' ? node.defaultVariant : node,
137-
)
138-
: undefined
139-
140-
// Handle INSTANCE nodes - treat as component reference
143+
// Handle INSTANCE nodes first — they only need position props (all sync),
144+
// skipping the expensive full getProps() with 6 async Figma API calls.
141145
if (node.type === 'INSTANCE') {
142-
const [props, mainComponent] = await Promise.all([
143-
propsPromise,
144-
node.getMainComponentAsync(),
145-
])
146+
const mainComponent = await node.getMainComponentAsync()
146147
if (mainComponent) await this.addComponentTree(mainComponent)
147148

148149
const componentName = getComponentName(mainComponent || node)
149-
150-
// Extract variant props from instance's componentProperties
151150
const variantProps = extractInstanceVariantProps(node)
152151

153-
// Check if needs position wrapper
154-
if (props.pos) {
152+
// Only compute position + transform (sync, no Figma API calls)
153+
const posProps = getPositionProps(node)
154+
if (posProps?.pos) {
155+
const transformProps = getTransformProps(node)
155156
perfEnd('buildTree()', tBuild)
156157
return {
157158
component: 'Box',
158159
props: {
159-
pos: props.pos,
160-
top: props.top,
161-
left: props.left,
162-
right: props.right,
163-
bottom: props.bottom,
164-
transform: props.transform,
160+
pos: posProps.pos,
161+
top: posProps.top,
162+
left: posProps.left,
163+
right: posProps.right,
164+
bottom: posProps.bottom,
165+
transform: posProps.transform || transformProps?.transform,
165166
w:
166167
(getPageNode(node as BaseNode & ChildrenMixin) as SceneNode)
167168
?.width === node.width
@@ -194,26 +195,36 @@ export class Codegen {
194195
}
195196
}
196197

197-
// Build children in parallel — each subtree is independent
198-
const childrenPromise =
199-
'children' in node
200-
? Promise.all(
201-
(node.children as SceneNode[]).map(async (child) => {
202-
if (child.type === 'INSTANCE') {
203-
const mainComponent = await child.getMainComponentAsync()
204-
if (mainComponent) await this.addComponentTree(mainComponent)
205-
}
206-
return this.buildTree(child)
207-
}),
208-
)
209-
: Promise.resolve([] as NodeTree[])
210-
211-
// Wait for props, children, and component tree in parallel
212-
const [props, children] = await Promise.all([
213-
propsPromise,
214-
childrenPromise,
215-
componentTreePromise,
216-
])
198+
// Fire getProps early for non-INSTANCE nodes — it runs while we process children.
199+
const propsPromise = getProps(node)
200+
201+
// Handle COMPONENT_SET or COMPONENT - add to componentTrees
202+
if (
203+
(node.type === 'COMPONENT_SET' || node.type === 'COMPONENT') &&
204+
((this.node.type === 'COMPONENT_SET' &&
205+
node === this.node.defaultVariant) ||
206+
this.node.type === 'COMPONENT')
207+
) {
208+
await this.addComponentTree(
209+
node.type === 'COMPONENT_SET' ? node.defaultVariant : node,
210+
)
211+
}
212+
213+
// Build children sequentially to avoid Figma API contention.
214+
// getProps(node) is already in-flight concurrently above.
215+
const children: NodeTree[] = []
216+
if ('children' in node) {
217+
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+
}
222+
children.push(await this.buildTree(child))
223+
}
224+
}
225+
226+
// Now await props (likely already resolved while children were processing)
227+
const props = await propsPromise
217228

218229
// Handle TEXT nodes
219230
let textChildren: string[] | undefined
@@ -280,25 +291,27 @@ export class Codegen {
280291
): Promise<void> {
281292
const tAdd = perfStart()
282293

283-
// Build children and get props+selectorProps in parallel
284-
const childrenPromise =
285-
'children' in node
286-
? Promise.all(
287-
(node.children as SceneNode[]).map(async (child) => {
288-
if (child.type === 'INSTANCE') {
289-
const mainComponent = await child.getMainComponentAsync()
290-
if (mainComponent) await this.addComponentTree(mainComponent)
291-
}
292-
return this.buildTree(child)
293-
}),
294-
)
295-
: Promise.resolve([] as NodeTree[])
296-
294+
// Fire getProps + getSelectorProps early (2 independent API calls)
295+
const propsPromise = getProps(node)
297296
const t = perfStart()
298-
const [childrenTrees, props, selectorProps] = await Promise.all([
299-
childrenPromise,
300-
getProps(node),
301-
getSelectorProps(node),
297+
const selectorPropsPromise = getSelectorProps(node)
298+
299+
// Build children sequentially to avoid Figma API contention
300+
const childrenTrees: NodeTree[] = []
301+
if ('children' in node) {
302+
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+
}
307+
childrenTrees.push(await this.buildTree(child))
308+
}
309+
}
310+
311+
// Await props + selectorProps (likely already resolved while children built)
312+
const [props, selectorProps] = await Promise.all([
313+
propsPromise,
314+
selectorPropsPromise,
302315
])
303316
perfEnd('getSelectorProps()', t)
304317

0 commit comments

Comments
 (0)