🐛 Stop thumbnail subscribers re-rendering on every store change

A thumbnail loaded from the server keeps a media id as :uri, and
workspace-thumbnail-by-id resolves it into a new map on each read. The
ref compared values with identical?, so every root frame, every
component in the assets panel and the interactions layer re-rendered
on every store change, mouse moves included. Compare by value instead.

This does not loop by itself, but it adds work to every commit and
brings other update loops closer to React's limit.

Relates to #12580

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco 2026-10-23 12:47:05 +02:00
parent 6508449fa0
commit 379470bdd2
2 changed files with 25 additions and 1 deletions

View File

@ -609,6 +609,8 @@
(defn workspace-thumbnail-by-id
[object-id]
;; Resolving a media id builds a new map on each read; compare by
;; value so subscribers do not re-render on every store change.
(l/derived
(fn [state]
(when-let [entry (dm/get-in state [:thumbnails object-id])]
@ -616,7 +618,8 @@
(and (:uri entry)
(not (resolved-uri? (:uri entry))))
(update :uri cf/resolve-media))))
st/state))
st/state
=))
(def workspace-text-modifier
(l/derived :workspace-text-modifier st/state))

View File

@ -8,6 +8,7 @@
(:require
[app.common.uuid :as uuid]
[app.main.refs :as refs]
[app.main.store :as st]
[cljs.test :as t :include-macros true]))
(def ^:private resolved-uri? @#'refs/resolved-uri?)
@ -28,3 +29,23 @@
(t/is (false? (resolved-uri? (uuid/next))))
(t/is (false? (resolved-uri? nil)))
(t/is (false? (resolved-uri? 42))))
(t/deftest thumbnail-ref-ignores-unrelated-state-changes
;; A thumbnail loaded from the server keeps a media id as :uri, and
;; the ref resolves it to a new map on each read. Subscribers (root
;; frames, assets) must not re-render on every unrelated store change.
(let [object-id (str (uuid/next))
ref (refs/workspace-thumbnail-by-id object-id)
calls (atom 0)]
(swap! st/state assoc-in [:thumbnails object-id] {:uri (uuid/next) :rendered-at nil})
(add-watch ref ::probe (fn [_ _ _ _] (swap! calls inc)))
(try
(swap! st/state assoc ::probe (uuid/next))
(t/is (zero? @calls) "unrelated change does not notify")
(swap! st/state assoc-in [:thumbnails object-id :uri] (uuid/next))
(t/is (= 1 @calls) "a new thumbnail uri notifies")
(finally
(remove-watch ref ::probe)
(swap! st/state #(-> %
(dissoc ::probe)
(update :thumbnails dissoc object-id)))))))