Add micro optimizations to layer-item component

This commit is contained in:
Andrey Antukh 2026-02-05 14:12:03 +01:00
parent a728d5a5f2
commit a080a9e646
3 changed files with 130 additions and 98 deletions

View File

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.math :as mth]
[app.common.types.component :as ctk] [app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
@ -186,26 +187,54 @@
children])) children]))
;; Memoized for performance
(mf/defc layer-item* (mf/defc layer-item*
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [index item selected objects is-sortable is-filtered depth parent-size is-component-child highlighted style render-children] [{:keys [index item selected objects
is-sortable is-filtered depth is-component-child
highlighted style render-children parent-size]
:or {render-children true}}] :or {render-children true}}]
(let [id (:id item) (let [id (get item :id)
blocked? (:blocked item) blocked? (get item :blocked)
hidden? (:hidden item) hidden? (get item :hidden)
shapes (get item :shapes)
shapes (mf/with-memo [shapes objects]
(loop [counter 0
shapes (seq shapes)
result (list)]
(if-let [id (first shapes)]
(if-let [obj (get objects id)]
(do
;; NOTE: this is a bit hacky, but reduces substantially
;; the allocation; If we use enumeration, we allocate
;; new sequence and add one iteration on each render,
;; independently if objects are changed or not. If we
;; store counter on metadata, we still need to create a
;; new allocation for each shape; with this method we
;; bypass this by mutating a private property on the
;; object removing extra allocation and extra iteration
;; on every request.
(unchecked-set obj "__$__counter" counter)
(recur (inc counter)
(rest shapes)
(conj result obj)))
(recur (inc counter)
(rest shapes)
result))
(-> result vec not-empty))))
drag-disabled* (mf/use-state false) drag-disabled* (mf/use-state false)
drag-disabled? (deref drag-disabled*) drag-disabled? (deref drag-disabled*)
scroll-middle-ref (mf/use-ref true) scroll-middle-ref (mf/use-ref true)
expanded-iref (mf/with-memo [id] expanded-iref (mf/with-memo [id]
(-> (l/in [:expanded id]) (l/derived #(dm/get-in % [:expanded id]) refs/workspace-local))
(l/derived refs/workspace-local)))
is-expanded (mf/deref expanded-iref) is-expanded (mf/deref expanded-iref)
is-selected (contains? selected id) is-selected (contains? selected id)
is-highlighted (contains? highlighted id) is-highlighted (contains? highlighted id)
container? (or (cfh/frame-shape? item) container? (or (cfh/frame-shape? item)
(cfh/group-shape? item)) (cfh/group-shape? item))
@ -339,7 +368,8 @@
:else :else
(cfh/get-parent-id objects id)) (cfh/get-parent-id objects id))
[parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files) [parent-id _]
(ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
parent (get objects parent-id) parent (get objects parent-id)
current-index (d/index-of (:shapes parent) id) current-index (d/index-of (:shapes parent) id)
@ -381,10 +411,9 @@
:index index :index index
:name (:name item)} :name (:name item)}
;; We don't want to change the structure of component copies ;; We don't want to change the structure of component copies
:draggable? (and :draggable? (and ^boolean is-sortable
is-sortable ^boolean (not is-read-only)
(not is-read-only) ^boolean (not (ctn/has-any-copy-parent? objects item))))]
(not (ctn/has-any-copy-parent? objects item))))]
(mf/with-effect [is-selected selected] (mf/with-effect [is-selected selected]
(let [single? (= (count selected) 1) (let [single? (= (count selected) 1)
@ -411,40 +440,47 @@
;; Setup scroll-driven lazy loading when expanded ;; Setup scroll-driven lazy loading when expanded
;; and ensures selected children are loaded immediately ;; and ensures selected children are loaded immediately
(mf/with-effect [is-expanded (:shapes item) selected] (mf/with-effect [is-expanded shapes selected]
(let [shapes-vec (:shapes item) (let [total (count shapes)]
total (count shapes-vec)] (if ^boolean is-expanded
(if is-expanded
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec (let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
;; Find if any selected id is a direct child and get its render index ;; Find if any selected id is a direct child and get its render index
selected-child-render-idx selected-child-render-idx
(when (and (> total default-chunk-size) (seq selected)) (when (> total default-chunk-size)
(let [shapes-reversed (vec (reverse shapes-vec))] (some (fn [sel-id]
(some (fn [sel-id] (let [idx (.indexOf shapes sel-id)]
(let [idx (.indexOf shapes-reversed sel-id)] (when (>= idx 0) idx)))
(when (>= idx 0) idx))) selected))
selected)))
;; Load at least enough to include the selected child plus extra ;; Load at least enough to include the selected child plus extra
;; for context (so it can be centered in the scroll view) ;; for context (so it can be centered in the scroll view)
min-count (if selected-child-render-idx min-count
(+ selected-child-render-idx default-chunk-size) (if selected-child-render-idx
default-chunk-size) (+ selected-child-render-idx default-chunk-size)
current @children-count* default-chunk-size)
new-count (min total (max current default-chunk-size min-count))]
current-count
@children-count*
new-count
(mth/min total (mth/max current-count default-chunk-size min-count))]
(reset! children-count* new-count)) (reset! children-count* new-count))
(reset! children-count* 0)))
(fn [] (reset! children-count* 0))
(when-let [obs (mf/ref-val observer-ref)]
(.disconnect obs) (fn []
(mf/set-ref-val! obs nil)))) (when-let [obs (mf/ref-val observer-ref)]
(.disconnect obs)
(mf/set-ref-val! obs nil)))))
;; Re-observe sentinel whenever children-count changes (sentinel moves) ;; Re-observe sentinel whenever children-count changes (sentinel moves)
;; and (shapes item) to reconnect observer after shape changes ;; and (shapes item) to reconnect observer after shape changes
(mf/with-effect [children-count is-expanded (:shapes item)] (mf/with-effect [children-count is-expanded shapes]
(let [total (count (:shapes item)) (let [total (count shapes)
node (mf/ref-val name-node-ref) name-node (mf/ref-val name-node-ref)
scroll-node (dom/get-parent-with-data node "scroll-container") scroll-node (dom/get-parent-with-data name-node "scroll-container")
lazy-node (mf/ref-val lazy-ref)] lazy-node (mf/ref-val lazy-ref)]
;; Disconnect previous observer ;; Disconnect previous observer
(when-let [obs (mf/ref-val observer-ref)] (when-let [obs (mf/ref-val observer-ref)]
@ -452,16 +488,15 @@
(mf/set-ref-val! observer-ref nil)) (mf/set-ref-val! observer-ref nil))
;; Setup new observer if there are more children to load ;; Setup new observer if there are more children to load
(when (and is-expanded (when (and ^boolean is-expanded
(< children-count total) ^boolean (< children-count total)
scroll-node ^boolean scroll-node
lazy-node) ^boolean lazy-node)
(let [cb (fn [entries] (let [cb (fn [entries]
(when (and (seq entries) (when (and (pos? (alength entries))
(.-isIntersecting (first entries))) (.-isIntersecting ^js (aget entries 0)))
;; Load next chunk when sentinel intersects ;; Load next chunk when sentinel intersects
(let [current @children-count* (let [next-count (mth/min total (+ children-count default-chunk-size))]
next-count (min total (+ current default-chunk-size))]
(reset! children-count* next-count)))) (reset! children-count* next-count))))
observer (js/IntersectionObserver. cb #js {:root scroll-node})] observer (js/IntersectionObserver. cb #js {:root scroll-node})]
(.observe observer lazy-node) (.observe observer lazy-node)
@ -494,29 +529,27 @@
:on-toggle-blocking toggle-blocking :on-toggle-blocking toggle-blocking
:style style} :style style}
(when (and render-children (when (and ^boolean render-children
(:shapes item) ^boolean shapes
is-expanded) ^boolean is-expanded)
[:div {:class (stl/css-case [:div {:class (stl/css-case
:element-children true :element-children true
:parent-selected is-selected :parent-selected is-selected
:sticky-children parent-board?) :sticky-children parent-board?)
:data-testid (dm/str "children-" id)} :data-testid (dm/str "children-" id)}
(let [all-children (reverse (d/enumerate (:shapes item))) (for [item (take children-count shapes)]
visible (take children-count all-children)] [:> layer-item*
(for [[index id] visible] {:item item
(when-let [item (get objects id)] :highlighted highlighted
[:> layer-item* :selected selected
{:item item :index (unchecked-get item "__$__counter")
:highlighted highlighted :objects objects
:selected selected :key (dm/str (get item :id))
:index index :is-sortable is-sortable
:objects objects :depth depth
:key (dm/str id) :parent-size parent-size
:is-sortable is-sortable :is-component-child is-component-tree}])
:depth depth
:parent-size parent-size (when (< children-count (count shapes))
:is-component-child is-component-tree}])))
(when (< children-count (count (:shapes item)))
[:div {:ref lazy-ref [:div {:ref lazy-ref
:class (stl/css :lazy-load-sentinel)}])])])) :class (stl/css :lazy-load-sentinel)}])])]))

View File

@ -22,12 +22,11 @@
(def ^:private space-for-icons 110) (def ^:private space-for-icons 110)
(def lens:shape-for-rename (def lens:shape-for-rename
(-> (l/in [:workspace-local :shape-for-rename]) (-> #(dm/get-in % [:workspace-local :shape-for-rename])
(l/derived st/state))) (l/derived st/state)))
(mf/defc layer-name* (mf/defc layer-name*
{::mf/wrap-props false {::mf/forward-ref true}
::mf/forward-ref true}
[{:keys [shape-id shape-name is-shape-touched disabled-double-click [{:keys [shape-id shape-name is-shape-touched disabled-double-click
on-start-edit on-stop-edit depth parent-size is-selected on-start-edit on-stop-edit depth parent-size is-selected
type-comp type-frame component-id is-hidden is-blocked type-comp type-frame component-id is-hidden is-blocked

View File

@ -78,36 +78,35 @@
(mf/defc layers-tree* (mf/defc layers-tree*
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [objects is-filtered parent-size] :as props}] [{:keys [objects is-filtered parent-size] :as props}]
(let [selected (use-selected-shapes) (let [selected (use-selected-shapes)
highlighted (mf/deref highlighted-shapes-ref) highlighted (mf/deref highlighted-shapes-ref)
root (get objects uuid/zero) root (get objects uuid/zero)
shapes (get root :shapes) shapes (get root :shapes)
shapes (mf/with-memo [shapes objects] shapes (mf/with-memo [shapes objects]
(loop [counter 0 (loop [counter 0
shapes (seq shapes) shapes (seq shapes)
result (list)] result (list)]
(if-let [id (first shapes)]
(if-let [id (first shapes)] (if-let [obj (get objects id)]
(if-let [obj (get objects id)] (do
(do ;; NOTE: this is a bit hacky, but reduces substantially
;; NOTE: this is a bit hacky, but reduces substantially ;; the allocation; If we use enumeration, we allocate
;; the allocation; If we use enumeration, we allocate ;; new sequence and add one iteration on each render,
;; new sequence and add one iteration on each render, ;; independently if objects are changed or not. If we
;; independently if objects are changed or not. If we ;; store counter on metadata, we still need to create a
;; store counter on metadata, we still need to create a ;; new allocation for each shape; with this method we
;; new allocation for each shape; with this method we ;; bypass this by mutating a private property on the
;; bypass this by mutating a private property on the ;; object removing extra allocation and extra iteration
;; object removing extra allocation and extra iteration ;; on every request.
;; on every request. (unchecked-set obj "__$__counter" counter)
(unchecked-set obj "__$__counter" counter) (recur (inc counter)
(recur (inc counter) (rest shapes)
(rest shapes) (conj result obj)))
(conj result obj))) (recur (inc counter)
(recur (inc counter) (rest shapes)
(rest shapes) result))
result)) result)))]
result)))]
[:div {:class (stl/css :element-list) :data-testid "layer-item"} [:div {:class (stl/css :element-list) :data-testid "layer-item"}
[:> hooks/sortable-container* {} [:> hooks/sortable-container* {}
@ -194,6 +193,7 @@
keys keys
(filter #(not= uuid/zero %)) (filter #(not= uuid/zero %))
vec)] vec)]
(update reparented-objects uuid/zero assoc :shapes reparented-shapes))) (update reparented-objects uuid/zero assoc :shapes reparented-shapes)))
;; --- Layers Toolbox ;; --- Layers Toolbox