Reduce allocation on layers-tree component

This commit is contained in:
Andrey Antukh 2026-02-05 12:41:57 +01:00
parent e2576d049a
commit 41f2877801
2 changed files with 84 additions and 46 deletions

View File

@ -183,9 +183,6 @@
[id] [id]
(l/derived #(contains? % id) selected-shapes)) (l/derived #(contains? % id) selected-shapes))
(def highlighted-shapes
(l/derived :highlighted workspace-local))
(def export-in-progress? (def export-in-progress?
(l/derived :export-in-progress? export)) (l/derived :export-in-progress? export))

View File

@ -31,31 +31,47 @@
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[goog.events :as events] [goog.events :as events]
[okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private highlighted-shapes-ref
(l/derived (fn [local]
(-> local
(get :highlighted)
(not-empty)))
refs/workspace-local))
(defn use-selected-shapes
"A convencience hook wrapper for get selected shapes"
[]
(let [selected (mf/deref refs/selected-shapes)]
(hooks/use-equal-memo selected)))
;; This components is a piece for sharding equality check between top ;; This components is a piece for sharding equality check between top
;; level frames and try to avoid rerender frames that are does not ;; level frames and try to avoid rerender frames that are does not
;; affected by the selected set. ;; affected by the selected set.
(mf/defc frame-wrapper* (mf/defc frame-wrapper*
[{:keys [selected] :as props}] [{:keys [selected] :as props}]
(let [pending-selected (mf/use-var selected) (let [pending-selected-ref
current-selected (mf/use-state selected) (mf/use-ref selected)
props (mf/spread-object props {:selected @current-selected})
current-selected
(mf/use-state selected)
props
(mf/spread-object props {:selected @current-selected})
set-selected set-selected
(mf/use-memo (mf/with-memo []
(fn [] (throttle-fn 50 #(when-let [pending-selected (mf/ref-val pending-selected-ref)]
(throttle-fn (reset! current-selected pending-selected))))]
50
#(when-let [pending-selected @pending-selected]
(reset! current-selected pending-selected)))))]
(mf/with-effect [selected set-selected] (mf/with-effect [selected set-selected]
(reset! pending-selected selected) (mf/set-ref-val! pending-selected-ref selected)
(set-selected) (^function set-selected)
(fn [] (fn []
(reset! pending-selected nil) (mf/set-ref-val! pending-selected-ref nil)
#(rx/dispose! set-selected))) (rx/dispose! set-selected)))
[:> layer-item* props])) [:> layer-item* props]))
@ -63,38 +79,63 @@
{::mf/wrap [mf/memo #(mf/throttle % 200)] {::mf/wrap [mf/memo #(mf/throttle % 200)]
::mf/wrap-props false} ::mf/wrap-props false}
[{:keys [objects is-filtered parent-size] :as props}] [{:keys [objects is-filtered parent-size] :as props}]
(let [selected (mf/deref refs/selected-shapes) (let [selected (use-selected-shapes)
selected (hooks/use-equal-memo selected) highlighted (mf/deref highlighted-shapes-ref)
highlighted (mf/deref refs/highlighted-shapes) root (get objects uuid/zero)
highlighted (hooks/use-equal-memo highlighted)
root (get objects uuid/zero)] shapes (get root :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)))]
[: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* {}
(for [[index id] (reverse (d/enumerate (:shapes root)))] (for [obj shapes]
(when-let [obj (get objects id)] (if (cfh/frame-shape? obj)
(if (cfh/frame-shape? obj) [:> frame-wrapper*
[:> frame-wrapper* {:item obj
{:item obj :selected selected
:selected selected :highlighted highlighted
:highlighted highlighted :index (unchecked-get obj "__$__counter")
:index index :objects objects
:objects objects :key (dm/str (get obj :id))
:key id :is-sortable true
:is-sortable true :is-filtered is-filtered
:is-filtered is-filtered :parent-size parent-size
:parent-size parent-size :depth -1}]
:depth -1}] [:> layer-item*
[:> layer-item* {:item obj
{:item obj :selected selected
:selected selected :highlighted highlighted
:highlighted highlighted :index (unchecked-get obj "__$__counter")
:index index :objects objects
:objects objects :key (dm/str (get obj :id))
:key id :is-sortable true
:is-sortable true :is-filtered is-filtered
:is-filtered is-filtered :depth -1
:depth -1 :parent-size parent-size}]))]]))
:parent-size parent-size}])))]]))
(mf/defc filters-tree (mf/defc filters-tree
{::mf/wrap [mf/memo #(mf/throttle % 200)] {::mf/wrap [mf/memo #(mf/throttle % 200)]