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
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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -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}]

View File

@ -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])