⚡ Speed up sidebar menus for large selections (#12028)

Reuse extracted colors for unchanged shapes and cache the text attrs
read from each text content.
This commit is contained in:
Elena Torró 2026-10-02 12:05:25 +02:00 committed by GitHub
parent c148ba5267
commit 822dbd6517
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 124 additions and 43 deletions

View File

@ -1351,6 +1351,43 @@
:shape-id (:shape-id fill) :shape-id (:shape-id fill)
:index (:index fill)}))) :index (:index fill)})))
(defn shape-colors
[shape file-id libraries]
(let [applied-tokens (:applied-tokens shape)
applied-fill (get applied-tokens :fill)
applied-stroke (get applied-tokens :stroke-color)
fills (:fills shape)
strokes (:strokes shape)
shadows (:shadow shape)
shape-id (:id shape)
fills* (map-indexed
(fn [index fill]
(cond-> (assoc fill :shape-id shape-id :index index)
(and (zero? index) applied-fill)
(assoc :has-token-applied true
:token-name applied-fill)))
fills)
strokes* (map-indexed
(fn [index stroke]
(cond-> (assoc stroke :shape-id shape-id :index index)
(and (zero? index) applied-stroke)
(assoc :has-token-applied true
:token-name applied-stroke)))
strokes)
shadows* (map-indexed #(assoc %2 :shape-id shape-id :index %1) shadows)]
(if (= :text (:type shape))
(-> []
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)
(into (extract-text-colors shape file-id libraries)))
(-> []
(into (keep #(fill->color-att % file-id libraries)) fills*)
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)))))
(defn extract-all-colors (defn extract-all-colors
"Extracts color information from a list of shapes, including fills, strokes, and shadows. "Extracts color information from a list of shapes, including fills, strokes, and shadows.
If a shape has applied tokens of type :fill or :stroke-color, the first fill or stroke If a shape has applied tokens of type :fill or :stroke-color, the first fill or stroke
@ -1366,41 +1403,4 @@
Returns: Returns:
A vector of color attribute maps with metadata for each shape." A vector of color attribute maps with metadata for each shape."
[shapes file-id libraries] [shapes file-id libraries]
(reduce (into [] (mapcat #(shape-colors % file-id libraries)) shapes))
(fn [result shape]
(let [applied-tokens (:applied-tokens shape)
applied-fill (get applied-tokens :fill)
applied-stroke (get applied-tokens :stroke-color)
fills (:fills shape)
strokes (:strokes shape)
shadows (:shadow shape)
shape-id (:id shape)
fills* (map-indexed
(fn [index fill]
(cond-> (assoc fill :shape-id shape-id :index index)
(and (zero? index) applied-fill)
(assoc :has-token-applied true
:token-name applied-fill)))
fills)
strokes* (map-indexed
(fn [index stroke]
(cond-> (assoc stroke :shape-id shape-id :index index)
(and (zero? index) applied-stroke)
(assoc :has-token-applied true
:token-name applied-stroke)))
strokes)
shadows* (map-indexed #(assoc %2 :shape-id shape-id :index %1) shadows)]
(if (= :text (:type shape))
(-> result
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)
(into (extract-text-colors shape file-id libraries)))
(-> result
(into (keep #(fill->color-att % file-id libraries)) fills*)
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)))))
[]
shapes))

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.types.library :as ctl]
[app.main.data.workspace.colors :as dwc] [app.main.data.workspace.colors :as dwc]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
@ -17,6 +18,17 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn- cached-shape-colors
[prev-cache cache libraries-colors shape file-id libraries]
(let [inputs [(:type shape) (:fills shape) (:strokes shape) (:shadow shape)
(:applied-tokens shape) (:content shape) file-id libraries-colors]
[cached-inputs cached] (when prev-cache (.get prev-cache (:id shape)))
colors (if (and cached-inputs (every? true? (map identical? cached-inputs inputs)))
cached
(dwc/shape-colors shape file-id libraries))]
(.set cache (:id shape) [inputs colors])
colors))
(defn- prepare-colors (defn- prepare-colors
"Prepares and groups extracted color information from shapes. "Prepares and groups extracted color information from shapes.
Input: Input:
@ -62,8 +74,11 @@
This structure allows fast lookups of all shapes using the same visual color, This structure allows fast lookups of all shapes using the same visual color,
regardless of whether it comes from local fills, strokes or shadow-colors." regardless of whether it comes from local fills, strokes or shadow-colors."
[shapes file-id libraries] [prev-cache cache libraries-colors shapes file-id libraries]
(let [data (into [] (remove nil?) (dwc/extract-all-colors shapes file-id libraries)) (let [data (into []
(comp (mapcat #(cached-shape-colors prev-cache cache libraries-colors % file-id libraries))
(remove nil?))
shapes)
groups (d/group-by :attrs #(dissoc % :attrs) data) groups (d/group-by :attrs #(dissoc % :attrs) data)
;; Unique color attribute maps ;; Unique color attribute maps
@ -98,9 +113,19 @@
(mf/defc color-selection-menu* (mf/defc color-selection-menu*
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
[{:keys [shapes file-id libraries]}] [{:keys [shapes file-id libraries]}]
(let [{:keys [groups library-colors colors token-colors]} (let [cache* (mf/use-ref nil)
libraries-colors* (mf/use-ref nil)
{:keys [groups library-colors colors token-colors]}
(mf/with-memo [file-id shapes libraries] (mf/with-memo [file-id shapes libraries]
(prepare-colors shapes file-id libraries)) (let [current (mapv (fn [[id library]] [id (ctl/get-colors (:data library))]) libraries)
previous (mf/ref-val libraries-colors*)
libraries-colors (if (= previous current) previous current)
cache (js/Map.)
result (prepare-colors (mf/ref-val cache*) cache libraries-colors shapes file-id libraries)]
(mf/set-ref-val! libraries-colors* libraries-colors)
(mf/set-ref-val! cache* cache)
result))
open* (mf/use-state true) open* (mf/use-state true)
open? (deref open*) open? (deref open*)

View File

@ -262,6 +262,20 @@
(assoc :layout-padding (merge-layout-padding values shape-values)))] (assoc :layout-padding (merge-layout-padding values shape-values)))]
(promote-simple-layout-padding-type merged-values))) (promote-simple-layout-padding-type merged-values)))
(def ^:private content-attrs-cache (js/WeakMap.))
(defn- get-content-attrs
"Moving a text keeps its `:content`, so the attrs read from it are cached by content."
[{:keys [content] :as shape} attrs]
(if (some? content)
(let [[cached-attrs cached] (.get content-attrs-cache content)]
(if (identical? cached-attrs attrs)
cached
(let [result (attrs/get-text-attrs-multi shape txt/default-text-attrs attrs)]
(.set content-attrs-cache content [attrs result])
result)))
(attrs/get-text-attrs-multi shape txt/default-text-attrs attrs)))
(defn get-attrs* (defn get-attrs*
"Given a group of attributes that we want to extract and the shapes to extract them from "Given a group of attributes that we want to extract and the shapes to extract them from
returns a list of tuples [id, values] with the extracted properties for the shapes that returns a list of tuples [id, values] with the extracted properties for the shapes that
@ -359,7 +373,7 @@
(let [shape-attrs (select-keys shape attrs) (let [shape-attrs (select-keys shape attrs)
content-attrs content-attrs
(attrs/get-text-attrs-multi shape txt/default-text-attrs attrs) (get-content-attrs shape attrs)
new-values new-values
(-> values (-> values

View File

@ -117,6 +117,7 @@
[frontend-tests.ui.shortcuts-labels-test] [frontend-tests.ui.shortcuts-labels-test]
[frontend-tests.ui.sidebar-scroll-test] [frontend-tests.ui.sidebar-scroll-test]
[frontend-tests.ui.stroke-menu-test] [frontend-tests.ui.stroke-menu-test]
[frontend-tests.ui.text-attrs-multiple-test]
[frontend-tests.util-clipboard-test] [frontend-tests.util-clipboard-test]
[frontend-tests.util-object-test] [frontend-tests.util-object-test]
[frontend-tests.util-queue-test] [frontend-tests.util-queue-test]
@ -254,6 +255,7 @@
'frontend-tests.ui.shortcuts-labels-test 'frontend-tests.ui.shortcuts-labels-test
'frontend-tests.ui.sidebar-scroll-test 'frontend-tests.ui.sidebar-scroll-test
'frontend-tests.ui.stroke-menu-test 'frontend-tests.ui.stroke-menu-test
'frontend-tests.ui.text-attrs-multiple-test
'frontend-tests.util-clipboard-test 'frontend-tests.util-clipboard-test
'frontend-tests.util-object-test 'frontend-tests.util-object-test
'frontend-tests.util-queue-test 'frontend-tests.util-queue-test

View File

@ -0,0 +1,40 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.ui.text-attrs-multiple-test
(:require
[app.main.ui.workspace.sidebar.options.shapes.multiple :as multiple]
[cljs.test :as t :include-macros true]))
(defn- content
[font-family]
{:type "root"
:children [{:type "paragraph-set"
:children [{:type "paragraph"
:children [{:text "a" :font-family font-family}]}]}]})
(def ^:private inter (content "Inter"))
(def ^:private objects
{:board-1 {:id :board-1 :type :frame :shapes [:inner :text-1]}
:inner {:id :inner :type :frame :shapes [:text-2]}
:board-2 {:id :board-2 :type :frame :shapes [:text-3 :rect]}
:text-1 {:id :text-1 :type :text :content inter :applied-tokens {:typography "body"}}
:text-2 {:id :text-2 :type :text :content inter :applied-tokens {:typography "body"}}
:text-3 {:id :text-3 :type :text :content (content "Roboto") :applied-tokens {:typography "body"}}
:rect {:id :rect :type :rect}})
(defn- text-attrs
[labels objects]
(multiple/get-attrs* (map objects labels) objects :text))
(t/deftest boards-read-nested-text-values
(let [[ids values tokens] (text-attrs [:board-1] objects)]
(t/is (= [:text-2 :text-1] ids))
(t/is (= "Inter" (:font-family values)))
(t/is (= "body" (:typography tokens))))
(let [[_ values] (text-attrs [:board-1 :board-2] objects)]
(t/is (= :multiple (:font-family values)))))