11import { getComponentName } from '../utils'
22import { getProps } from './props'
3+ import { getPositionProps } from './props/position'
34import { getSelectorProps } from './props/selector'
5+ import { getTransformProps } from './props/transform'
46import { renderComponent , renderNode } from './render'
57import { renderText } from './render/text'
68import 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