Skip to content

Commit 10285bd

Browse files
committed
Optimize
1 parent ff86ffb commit 10285bd

File tree

3 files changed

+151
-92
lines changed

3 files changed

+151
-92
lines changed

src/codegen/Codegen.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ export class Codegen {
108108
// Cache buildTree results by node.id to avoid duplicate subtree builds
109109
// (e.g., when addComponentTree and main tree walk process the same children)
110110
private buildTreeCache: Map<string, Promise<NodeTree>> = new Map()
111+
// Collect fire-and-forget addComponentTree promises so we can await them
112+
// before rendering component codes (decouples INSTANCE buildTree from addComponentTree)
113+
private pendingComponentTrees: Promise<void>[] = []
111114

112115
constructor(private node: SceneNode) {
113116
this.node = node
@@ -160,6 +163,12 @@ export class Codegen {
160163
this.tree = tree
161164
}
162165

166+
// Await all fire-and-forget addComponentTree calls before rendering
167+
if (this.pendingComponentTrees.length > 0) {
168+
await Promise.all(this.pendingComponentTrees)
169+
this.pendingComponentTrees = []
170+
}
171+
163172
// Sync componentTrees to components
164173
for (const [compId, compTree] of this.componentTrees) {
165174
if (!this.components.has(compId)) {
@@ -203,7 +212,15 @@ export class Codegen {
203212
this.buildTreeCache.set(cacheKey, promise)
204213
globalBuildTreeCache.set(cacheKey, promise)
205214
}
206-
return promise
215+
const result = await promise
216+
// When called as the root-level buildTree (node === this.node),
217+
// drain any fire-and-forget addComponentTree promises so that
218+
// getComponentTrees() is populated before the caller inspects it.
219+
if (node === this.node && this.pendingComponentTrees.length > 0) {
220+
await Promise.all(this.pendingComponentTrees)
221+
this.pendingComponentTrees = []
222+
}
223+
return result
207224
}
208225

209226
private async doBuildTree(node: SceneNode): Promise<NodeTree> {
@@ -237,7 +254,11 @@ export class Codegen {
237254
// skipping the expensive full getProps() with 6 async Figma API calls.
238255
if (node.type === 'INSTANCE') {
239256
const mainComponent = await getMainComponentCached(node)
240-
if (mainComponent) await this.addComponentTree(mainComponent)
257+
// Fire addComponentTree without awaiting — it runs in the background.
258+
// All pending promises are collected and awaited in run() before rendering.
259+
if (mainComponent) {
260+
this.pendingComponentTrees.push(this.addComponentTree(mainComponent))
261+
}
241262

242263
const componentName = getComponentName(mainComponent || node)
243264
const variantProps = extractInstanceVariantProps(node)
@@ -291,15 +312,17 @@ export class Codegen {
291312
// Fire getProps early for non-INSTANCE nodes — it runs while we process children.
292313
const propsPromise = getProps(node)
293314

294-
// Handle COMPONENT_SET or COMPONENT - add to componentTrees
315+
// Handle COMPONENT_SET or COMPONENT - add to componentTrees (fire-and-forget)
295316
if (
296317
(node.type === 'COMPONENT_SET' || node.type === 'COMPONENT') &&
297318
((this.node.type === 'COMPONENT_SET' &&
298319
node === this.node.defaultVariant) ||
299320
this.node.type === 'COMPONENT')
300321
) {
301-
await this.addComponentTree(
302-
node.type === 'COMPONENT_SET' ? node.defaultVariant : node,
322+
this.pendingComponentTrees.push(
323+
this.addComponentTree(
324+
node.type === 'COMPONENT_SET' ? node.defaultVariant : node,
325+
),
303326
)
304327
}
305328

@@ -347,6 +370,11 @@ export class Codegen {
347370
async getTree(): Promise<NodeTree> {
348371
if (!this.tree) {
349372
this.tree = await this.buildTree(this.node)
373+
// Await any fire-and-forget addComponentTree calls launched during buildTree
374+
if (this.pendingComponentTrees.length > 0) {
375+
await Promise.all(this.pendingComponentTrees)
376+
this.pendingComponentTrees = []
377+
}
350378
}
351379
return this.tree
352380
}

src/codegen/props/selector.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,19 @@ async function computeSelectorPropsForGroup(
273273
const result: Record<string, object | string> = {}
274274
const diffKeys = new Set<string>()
275275

276-
// Calculate diffs for each effect state
277-
for (const component of matchingComponents) {
278-
const effect = component.variantProperties?.effect
279-
if (!effect || effect === 'default') continue
280-
281-
const props = await getProps(component)
276+
// Calculate diffs for each effect state — fire all getProps() concurrently
277+
const effectComponents = matchingComponents.filter((c) => {
278+
const effect = c.variantProperties?.effect
279+
return effect && effect !== 'default'
280+
})
281+
const effectPropsResults = await Promise.all(
282+
effectComponents.map(async (component) => {
283+
const effect = component.variantProperties?.effect as string
284+
const props = await getProps(component)
285+
return { effect, props }
286+
}),
287+
)
288+
for (const { effect, props } of effectPropsResults) {
282289
const def = difference(props, defaultProps)
283290
if (Object.keys(def).length === 0) continue
284291

src/codegen/responsive/ResponsiveCodegen.ts

Lines changed: 105 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,16 @@ export class ResponsiveCodegen {
6464
return Codegen.renderTree(tree, 0)
6565
}
6666

67-
// Extract trees per breakpoint using Codegen.
68-
const breakpointTrees = new Map<BreakpointKey, NodeTree>()
69-
for (const [bp, node] of this.breakpointNodes) {
70-
const codegen = new Codegen(node)
71-
const tree = await codegen.getTree()
72-
breakpointTrees.set(bp, tree)
73-
}
67+
// Extract trees per breakpoint using Codegen — all independent, run in parallel.
68+
const breakpointEntries = [...this.breakpointNodes.entries()]
69+
const treeResults = await Promise.all(
70+
breakpointEntries.map(async ([bp, node]) => {
71+
const codegen = new Codegen(node)
72+
const tree = await codegen.getTree()
73+
return [bp, tree] as const
74+
}),
75+
)
76+
const breakpointTrees = new Map<BreakpointKey, NodeTree>(treeResults)
7477

7578
// Merge trees and generate code.
7679
return this.generateMergedCode(breakpointTrees, 0)
@@ -383,32 +386,37 @@ export class ResponsiveCodegen {
383386
}
384387
}
385388

386-
// Build trees for each viewport
387-
const treesByBreakpoint = new Map<BreakpointKey, NodeTree>()
388-
for (const [bp, component] of viewportComponents) {
389-
let t = perfStart()
390-
const codegen = new Codegen(component)
391-
const tree = await codegen.getTree()
392-
perfEnd('Codegen.getTree(viewportVariant)', t)
393-
394-
// Get pseudo-selector props for this specific variant group AND viewport
395-
// This ensures hover/active colors are correctly responsive per viewport
396-
if (effectKey) {
397-
const viewportValue = component.variantProperties?.[viewportKey]
398-
t = perfStart()
399-
const selectorProps = await getSelectorPropsForGroup(
400-
componentSet,
401-
variantFilter,
402-
viewportValue,
403-
)
404-
perfEnd('getSelectorPropsForGroup(viewport)', t)
405-
if (Object.keys(selectorProps).length > 0) {
406-
Object.assign(tree.props, selectorProps)
389+
// Build trees for each viewport — all independent, run in parallel.
390+
const viewportEntries = [...viewportComponents.entries()]
391+
const viewportTreeResults = await Promise.all(
392+
viewportEntries.map(async ([bp, component]) => {
393+
let t = perfStart()
394+
const codegen = new Codegen(component)
395+
const tree = await codegen.getTree()
396+
perfEnd('Codegen.getTree(viewportVariant)', t)
397+
398+
// Get pseudo-selector props for this specific variant group AND viewport
399+
// This ensures hover/active colors are correctly responsive per viewport
400+
if (effectKey) {
401+
const viewportValue = component.variantProperties?.[viewportKey]
402+
t = perfStart()
403+
const selectorProps = await getSelectorPropsForGroup(
404+
componentSet,
405+
variantFilter,
406+
viewportValue,
407+
)
408+
perfEnd('getSelectorPropsForGroup(viewport)', t)
409+
if (Object.keys(selectorProps).length > 0) {
410+
Object.assign(tree.props, selectorProps)
411+
}
407412
}
408-
}
409413

410-
treesByBreakpoint.set(bp, tree)
411-
}
414+
return [bp, tree] as const
415+
}),
416+
)
417+
const treesByBreakpoint = new Map<BreakpointKey, NodeTree>(
418+
viewportTreeResults,
419+
)
412420

413421
// Generate merged responsive code
414422
const mergedCode = responsiveCodegen.generateMergedCode(
@@ -592,35 +600,48 @@ export class ResponsiveCodegen {
592600
Map<BreakpointKey, NodeTree>
593601
>()
594602

595-
for (const [compositeKey, viewportComponents] of byCompositeVariant) {
596-
// Use original names for Figma data access
597-
const variantFilter = parseCompositeKeyToOriginal(compositeKey)
603+
// Build trees for all composite variants in parallel — each is independent.
604+
const compositeEntries = [...byCompositeVariant.entries()]
605+
const compositeResults = await Promise.all(
606+
compositeEntries.map(async ([compositeKey, viewportComponents]) => {
607+
// Use original names for Figma data access
608+
const variantFilter = parseCompositeKeyToOriginal(compositeKey)
609+
610+
// Build trees for each viewport within this composite — also parallel.
611+
const vpEntries = [...viewportComponents.entries()]
612+
const vpResults = await Promise.all(
613+
vpEntries.map(async ([bp, component]) => {
614+
let t = perfStart()
615+
const codegen = new Codegen(component)
616+
const tree = await codegen.getTree()
617+
perfEnd('Codegen.getTree(variant)', t)
618+
619+
// Get pseudo-selector props for this specific variant group AND viewport
620+
if (effectKey) {
621+
const viewportValue = component.variantProperties?.[viewportKey]
622+
t = perfStart()
623+
const selectorProps = await getSelectorPropsForGroup(
624+
componentSet,
625+
variantFilter,
626+
viewportValue,
627+
)
628+
perfEnd('getSelectorPropsForGroup()', t)
629+
if (Object.keys(selectorProps).length > 0) {
630+
Object.assign(tree.props, selectorProps)
631+
}
632+
}
598633

599-
const treesByBreakpoint = new Map<BreakpointKey, NodeTree>()
600-
for (const [bp, component] of viewportComponents) {
601-
let t = perfStart()
602-
const codegen = new Codegen(component)
603-
const tree = await codegen.getTree()
604-
perfEnd('Codegen.getTree(variant)', t)
605-
606-
// Get pseudo-selector props for this specific variant group AND viewport
607-
// This ensures hover/active colors are correctly responsive per viewport
608-
if (effectKey) {
609-
const viewportValue = component.variantProperties?.[viewportKey]
610-
t = perfStart()
611-
const selectorProps = await getSelectorPropsForGroup(
612-
componentSet,
613-
variantFilter,
614-
viewportValue,
615-
)
616-
perfEnd('getSelectorPropsForGroup()', t)
617-
if (Object.keys(selectorProps).length > 0) {
618-
Object.assign(tree.props, selectorProps)
619-
}
620-
}
634+
return [bp, tree] as const
635+
}),
636+
)
621637

622-
treesByBreakpoint.set(bp, tree)
623-
}
638+
return [
639+
compositeKey,
640+
new Map<BreakpointKey, NodeTree>(vpResults),
641+
] as const
642+
}),
643+
)
644+
for (const [compositeKey, treesByBreakpoint] of compositeResults) {
624645
responsivePropsByComposite.set(compositeKey, treesByBreakpoint)
625646
}
626647

@@ -705,29 +726,32 @@ export class ResponsiveCodegen {
705726
componentSet.componentPropertyDefinitions,
706727
).some((key) => key.toLowerCase() === 'effect')
707728

708-
// Build trees for each variant
709-
const treesByVariant = new Map<string, NodeTree>()
710-
for (const [variantValue, component] of componentsByVariant) {
711-
// Get pseudo-selector props for this specific variant group
712-
const variantFilter: Record<string, string> = {
713-
[primaryVariantKey]: variantValue,
714-
}
715-
let t = perfStart()
716-
const selectorProps = hasEffect
717-
? await getSelectorPropsForGroup(componentSet, variantFilter)
718-
: null
719-
perfEnd('getSelectorPropsForGroup(nonViewport)', t)
720-
721-
t = perfStart()
722-
const codegen = new Codegen(component)
723-
const tree = await codegen.getTree()
724-
perfEnd('Codegen.getTree(nonViewportVariant)', t)
725-
// Add pseudo-selector props to tree
726-
if (selectorProps && Object.keys(selectorProps).length > 0) {
727-
Object.assign(tree.props, selectorProps)
728-
}
729-
treesByVariant.set(variantValue, tree)
730-
}
729+
// Build trees for each variant — all independent, run in parallel.
730+
const variantEntries = [...componentsByVariant.entries()]
731+
const variantResults = await Promise.all(
732+
variantEntries.map(async ([variantValue, component]) => {
733+
// Get pseudo-selector props for this specific variant group
734+
const variantFilter: Record<string, string> = {
735+
[primaryVariantKey]: variantValue,
736+
}
737+
let t = perfStart()
738+
const selectorProps = hasEffect
739+
? await getSelectorPropsForGroup(componentSet, variantFilter)
740+
: null
741+
perfEnd('getSelectorPropsForGroup(nonViewport)', t)
742+
743+
t = perfStart()
744+
const codegen = new Codegen(component)
745+
const tree = await codegen.getTree()
746+
perfEnd('Codegen.getTree(nonViewportVariant)', t)
747+
// Add pseudo-selector props to tree
748+
if (selectorProps && Object.keys(selectorProps).length > 0) {
749+
Object.assign(tree.props, selectorProps)
750+
}
751+
return [variantValue, tree] as const
752+
}),
753+
)
754+
const treesByVariant = new Map<string, NodeTree>(variantResults)
731755

732756
// Generate merged code with variant conditionals
733757
const responsiveCodegen = new ResponsiveCodegen(null)

0 commit comments

Comments
 (0)