From 379470bdd22dd27bb83858d4b27ecd2b9d93d616 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 23 Oct 2026 12:47:05 +0200 Subject: [PATCH] :bug: 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 --- frontend/src/app/main/refs.cljs | 5 ++++- .../test/frontend_tests/main/refs_test.cljs | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 84f6bd72f2..bb7fc0392a 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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)) diff --git a/frontend/test/frontend_tests/main/refs_test.cljs b/frontend/test/frontend_tests/main/refs_test.cljs index 99c9f57657..581ff949d2 100644 --- a/frontend/test/frontend_tests/main/refs_test.cljs +++ b/frontend/test/frontend_tests/main/refs_test.cljs @@ -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)))))))