Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/web/src/components/nodes/FlowNode.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo } from 'react'
import { Handle, Position } from '@xyflow/react'
import type { NodeProps, Node } from '@xyflow/react'
import type { FlowData } from '../../types'
Expand Down Expand Up @@ -44,7 +45,7 @@ export type FlowNodeData = {

type FlowNodeType = Node<FlowNodeData, 'flowNode'>

export function FlowNodeComponent({ data, id }: NodeProps<FlowNodeType>) {
function FlowNodeComponentInner({ data, id }: NodeProps<FlowNodeType>) {
const {
label,
nodeType,
Expand Down Expand Up @@ -314,3 +315,16 @@ export function FlowNodeComponent({ data, id }: NodeProps<FlowNodeType>) {
</div>
)
}

// React Flow doesn't memoize custom node components — every viewport
// change (pan / zoom) emits a store update that re-renders every
// node. With 60+ nodes (e.g. the type-variants showcase flow), that
// reconciler churn dominates the frame budget. memo() short-circuits
// on shallow-equal data so renders only happen when the node's actual
// state changes (active, currentStep, isDynamic transitions, etc).
//
// Caveat: works because FlowCanvas's `nodes` useMemo holds data
// references stable across viewport changes — its deps list excludes
// the viewport. If we ever spread `node.data` from a non-memoized
// source, this becomes a no-op.
export const FlowNodeComponent = memo(FlowNodeComponentInner)
16 changes: 15 additions & 1 deletion packages/web/src/components/nodes/NodeBuilding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,21 @@ export function SpriteBuilding({

return (
<div
style={{ position: 'relative', width: SPRITE_SIZE, height: SPRITE_SIZE, overflow: 'visible' }}
style={{
position: 'relative',
width: SPRITE_SIZE,
height: SPRITE_SIZE,
overflow: 'visible',
// Promote the sprite to its own compositor layer so the
// rasterized image is cached. Without this, every zoom level
// change re-rasterizes the (large) SVG source through the CPU
// for each visible node — on flows with many nodes the cost
// compounds and pan/zoom drops frames. translate3d forces the
// layer on engines that need both hints; will-change tells
// Chrome we're about to transform so it pre-promotes.
willChange: 'transform',
transform: 'translate3d(0, 0, 0)',
}}
>
<img
src={src}
Expand Down
Loading