From 54aaebee1e028713b7a369e780499c16d2bfc8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Wed, 19 Aug 2026 11:50:28 +0200 Subject: [PATCH] :zap: Improve shape attrs parsing performance (#11259) * :zap: Memoize shape-attr->token-attrs and hoist per-type attrs in get-attrs* * :zap: Skip redundant token merges for token-less shapes in get-attrs* * :zap: Freeze group descendant attrs in design panel during transforms --- common/src/app/common/types/token.cljc | 21 ++++++-- .../sidebar/options/shapes/group.cljs | 30 ++++++++--- .../sidebar/options/shapes/multiple.cljs | 52 +++++++++++++------ 3 files changed, 77 insertions(+), 26 deletions(-) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 10cedd5c19..852d2529d5 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -423,11 +423,8 @@ :stroke-width :strokes token-attr)) -(defn shape-attr->token-attrs - "Returns the token-attr affected when a given attribute in a shape is changed. - The sub-attr is for attributes that may have multiple values, like strokes - (may be width or color) and layout padding & margin (may have 4 edges)." - ([shape-attr] (shape-attr->token-attrs shape-attr nil)) +(defn- shape-attr->token-attrs* + ([shape-attr] (shape-attr->token-attrs* shape-attr nil)) ([shape-attr changed-sub-attr] (cond (= :fills shape-attr) @@ -468,6 +465,20 @@ (number-keys shape-attr) #{shape-attr} (axis-keys shape-attr) #{shape-attr}))) +(def ^:private shape-attr->token-attrs-1 + (memoize shape-attr->token-attrs*)) + +(defn shape-attr->token-attrs + "Returns the token-attr affected when a given attribute in a shape is changed. + The sub-attr is for attributes that may have multiple values, like strokes + (may be width or color) and layout padding & margin (may have 4 edges)." + ([shape-attr] + (shape-attr->token-attrs-1 shape-attr)) + ([shape-attr changed-sub-attr] + (if (nil? changed-sub-attr) + (shape-attr->token-attrs-1 shape-attr) + (shape-attr->token-attrs* shape-attr changed-sub-attr)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS for token attributes by shape type ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index d0ae918bfe..8d4149dc7b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -93,20 +93,38 @@ [constraint-ids constraint-values] (get-attrs shapes objects :constraint) - [fill-ids fill-values fill-tokens] - (get-attrs shapes objects :fill) - [shadow-ids] (get-attrs shapes objects :shadow) [blur-ids blur-values] (get-attrs shapes objects :blur) + transform + (mf/deref refs/current-transform) + + ;; A transform cannot change the descendants read here. + descendant-attrs-ref + (mf/use-ref nil) + + descendant-attrs + (let [cached (mf/ref-val descendant-attrs-ref)] + (if (and (some? transform) (some? cached)) + cached + (let [attrs {:fill (get-attrs shapes objects :fill) + :stroke (get-attrs shapes objects :stroke) + :text (get-attrs shapes objects :text) + :colors (vals objects)}] + (mf/set-ref-val! descendant-attrs-ref attrs) + attrs))) + + [fill-ids fill-values fill-tokens] + (get descendant-attrs :fill) + [stroke-ids stroke-values stroke-tokens] - (get-attrs shapes objects :stroke) + (get descendant-attrs :stroke) [text-ids text-values text-tokens] - (get-attrs shapes objects :text) + (get descendant-attrs :text) [layout-item-ids layout-item-values] (get-attrs shapes objects :layout-item)] @@ -164,7 +182,7 @@ [:> color-selection-menu* {:type type - :shapes (vals objects) + :shapes (get descendant-attrs :colors) :file-id file-id :libraries libraries}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index f20dc82caa..8a0fca8b6d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -268,6 +268,21 @@ applies (some of them ignore some attributes)" [shapes objects attr-group] (let [attrs (group->attrs attr-group) + + type->editable-attrs + (memoize (fn [type] + (if-let [editable? (get editable-attrs type)] + (filterv editable? attrs) + []))) + + type->nil-values + (memoize (fn [type] (into {} (map (fn [attr] [attr nil])) (type->editable-attrs type)))) + + type->token-attrs + (memoize (fn [type] + (into [] (comp (mapcat tt/shape-attr->token-attrs) (distinct)) + (type->editable-attrs type)))) + merge-attrs (fn [v1 v2] (cond @@ -289,24 +304,31 @@ (= existing new-val) acc :else (assoc acc t-attr :multiple)))) - merge-shape-attr - (fn [acc applied-tokens shape-attr] - "Merges all token attributes derived from a single shape attribute - into the accumulator map using `merge-attr`." - (let [token-attrs (tt/shape-attr->token-attrs shape-attr)] - (reduce #(merge-attr %1 applied-tokens %2) acc token-attrs))) + ;; Merging an empty `applied-tokens` into an accumulator that a previous + ;; empty merge already produced is a fixed point, so long runs of + ;; token-less shapes of the same type only pay for the first one. + stable-token-acc (volatile! nil) merge-token-values - (fn [acc shape-attrs applied-tokens] - "Merges token values across all shape attributes. - For each shape attribute, its corresponding token attributes are merged - into the accumulator." - (reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs)) + (fn [acc token-attrs applied-tokens] + "Merges token values across all token attributes derived from the shape's + editable attributes." + (let [no-tokens? (empty? applied-tokens) + stable (deref stable-token-acc)] + (if (and no-tokens? + (some? stable) + (identical? (nth stable 0) token-attrs) + (identical? (nth stable 1) acc)) + acc + (let [result (reduce #(merge-attr %1 applied-tokens %2) acc token-attrs)] + (when no-tokens? + (vreset! stable-token-acc [token-attrs result])) + result)))) extract-attrs (fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}] (let [read-mode (get-in type->read-mode [type attr-group]) - editable-attrs (filter (get editable-attrs (:type shape)) attrs)] + editable-attrs (type->editable-attrs type)] (case read-mode :ignore [ids values] @@ -315,14 +337,14 @@ (let [;; Get the editable attrs from the shape, ensuring that all attributes ;; are present, with value nil if they are not present in the shape. shape-values (merge - (into {} (map #(vector % nil)) editable-attrs) + (type->nil-values type) (cond (= attr-group :measure) (select-measure-keys shape) :else (select-keys shape editable-attrs))) shape-values (cond-> shape-values (= attr-group :layer) (update :hidden #(if (nil? %) false %))) - new-token-acc (merge-token-values token-acc editable-attrs applied-tokens)] + new-token-acc (merge-token-values token-acc (type->token-attrs type) applied-tokens)] [(conj ids id) (merge-attrs values shape-values) new-token-acc]) @@ -338,7 +360,7 @@ (merge-attrs shape-attrs) (merge-attrs content-attrs)) - new-token-acc (merge-token-values token-acc editable-attrs applied-tokens)] + new-token-acc (merge-token-values token-acc (type->token-attrs type) applied-tokens)] [(conj ids id) new-values new-token-acc])