Improve shape attrs parsing performance (#11259)

*  Memoize shape-attr->token-attrs and hoist per-type attrs in get-attrs*

*  Skip redundant token merges for token-less shapes in get-attrs*

*  Freeze group descendant attrs in design panel during transforms
This commit is contained in:
Elena Torró 2026-08-19 11:50:28 +02:00 committed by GitHub
parent 8da13b5fa1
commit 54aaebee1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 77 additions and 26 deletions

View File

@ -423,11 +423,8 @@
:stroke-width :strokes :stroke-width :strokes
token-attr)) token-attr))
(defn shape-attr->token-attrs (defn- shape-attr->token-attrs*
"Returns the token-attr affected when a given attribute in a shape is changed. ([shape-attr] (shape-attr->token-attrs* shape-attr nil))
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))
([shape-attr changed-sub-attr] ([shape-attr changed-sub-attr]
(cond (cond
(= :fills shape-attr) (= :fills shape-attr)
@ -468,6 +465,20 @@
(number-keys shape-attr) #{shape-attr} (number-keys shape-attr) #{shape-attr}
(axis-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 ;; HELPERS for token attributes by shape type
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -93,20 +93,38 @@
[constraint-ids constraint-values] [constraint-ids constraint-values]
(get-attrs shapes objects :constraint) (get-attrs shapes objects :constraint)
[fill-ids fill-values fill-tokens]
(get-attrs shapes objects :fill)
[shadow-ids] [shadow-ids]
(get-attrs shapes objects :shadow) (get-attrs shapes objects :shadow)
[blur-ids blur-values] [blur-ids blur-values]
(get-attrs shapes objects :blur) (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] [stroke-ids stroke-values stroke-tokens]
(get-attrs shapes objects :stroke) (get descendant-attrs :stroke)
[text-ids text-values text-tokens] [text-ids text-values text-tokens]
(get-attrs shapes objects :text) (get descendant-attrs :text)
[layout-item-ids layout-item-values] [layout-item-ids layout-item-values]
(get-attrs shapes objects :layout-item)] (get-attrs shapes objects :layout-item)]
@ -164,7 +182,7 @@
[:> color-selection-menu* [:> color-selection-menu*
{:type type {:type type
:shapes (vals objects) :shapes (get descendant-attrs :colors)
:file-id file-id :file-id file-id
:libraries libraries}] :libraries libraries}]

View File

@ -268,6 +268,21 @@
applies (some of them ignore some attributes)" applies (some of them ignore some attributes)"
[shapes objects attr-group] [shapes objects attr-group]
(let [attrs (group->attrs 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 merge-attrs
(fn [v1 v2] (fn [v1 v2]
(cond (cond
@ -289,24 +304,31 @@
(= existing new-val) acc (= existing new-val) acc
:else (assoc acc t-attr :multiple)))) :else (assoc acc t-attr :multiple))))
merge-shape-attr ;; Merging an empty `applied-tokens` into an accumulator that a previous
(fn [acc applied-tokens shape-attr] ;; empty merge already produced is a fixed point, so long runs of
"Merges all token attributes derived from a single shape attribute ;; token-less shapes of the same type only pay for the first one.
into the accumulator map using `merge-attr`." stable-token-acc (volatile! nil)
(let [token-attrs (tt/shape-attr->token-attrs shape-attr)]
(reduce #(merge-attr %1 applied-tokens %2) acc token-attrs)))
merge-token-values merge-token-values
(fn [acc shape-attrs applied-tokens] (fn [acc token-attrs applied-tokens]
"Merges token values across all shape attributes. "Merges token values across all token attributes derived from the shape's
For each shape attribute, its corresponding token attributes are merged editable attributes."
into the accumulator." (let [no-tokens? (empty? applied-tokens)
(reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs)) 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 extract-attrs
(fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}] (fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}]
(let [read-mode (get-in type->read-mode [type attr-group]) (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 (case read-mode
:ignore :ignore
[ids values] [ids values]
@ -315,14 +337,14 @@
(let [;; Get the editable attrs from the shape, ensuring that all attributes (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. ;; are present, with value nil if they are not present in the shape.
shape-values (merge shape-values (merge
(into {} (map #(vector % nil)) editable-attrs) (type->nil-values type)
(cond (cond
(= attr-group :measure) (select-measure-keys shape) (= attr-group :measure) (select-measure-keys shape)
:else (select-keys shape editable-attrs))) :else (select-keys shape editable-attrs)))
shape-values (cond-> shape-values shape-values (cond-> shape-values
(= attr-group :layer) (= attr-group :layer)
(update :hidden #(if (nil? %) false %))) (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) [(conj ids id)
(merge-attrs values shape-values) (merge-attrs values shape-values)
new-token-acc]) new-token-acc])
@ -338,7 +360,7 @@
(merge-attrs shape-attrs) (merge-attrs shape-attrs)
(merge-attrs content-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) [(conj ids id)
new-values new-values
new-token-acc]) new-token-acc])