diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs
index 1d4c0098f..3355bfc3f 100644
--- a/frontend/src/app/main/data/workspace/texts.cljs
+++ b/frontend/src/app/main/data/workspace/texts.cljs
@@ -415,10 +415,14 @@
   [id]
   (ptk/reify ::clean-text-modifier
     ptk/WatchEvent
-    (watch [_ _ _]
-      (->> (rx/of #(update % :workspace-text-modifier dissoc id))
-           ;; We delay a bit the change so there is no weird transition to the user
-           (rx/delay 50)))))
+    (watch [_ state _]
+      (let [current-value (dm/get-in state [:workspace-text-modifier id])]
+        ;; We only dissocc the value when hasn't change after a time
+        (->> (rx/of (fn [state]
+                      (cond-> state
+                        (identical? (dm/get-in state [:workspace-text-modifier id]) current-value)
+                        (update :workspace-text-modifier dissoc id))))
+             (rx/delay 100))))))
 
 (defn remove-text-modifier
   [id]
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 2512826dc..b61f63749 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -601,9 +601,17 @@
                 move-events (->> stream
                                  (rx/filter (ptk/type? ::nudge-selected-shapes))
                                  (rx/filter #(= direction (deref %))))
-                stopper (->> move-events
-                             (rx/debounce 100)
-                             (rx/take 1))
+
+                stopper
+                (->> move-events
+                     ;; We stop when there's been 1s without movement or after 250ms after a key-up
+                     (rx/switch-map #(rx/merge
+                                      (rx/timer 1000)
+                                      (->> stream
+                                           (rx/filter ms/key-up?)
+                                           (rx/delay 250))))
+                     (rx/take 1))
+
                 scale (if shift? (gpt/point (or (:big nudge) 10)) (gpt/point (or (:small nudge) 1)))
                 mov-vec (gpt/multiply (get-displacement direction) scale)]
 
diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs
index 1a4cb1069..a586077a9 100644
--- a/frontend/src/app/main/streams.cljs
+++ b/frontend/src/app/main/streams.cljs
@@ -21,6 +21,16 @@
   [v]
   (instance? KeyboardEvent v))
 
+(defn key-up?
+  [v]
+  (and (keyboard-event? v)
+       (= :up (:type v))))
+
+(defn key-down?
+  [v]
+  (and (keyboard-event? v)
+       (= :down (:type v))))
+
 (defrecord MouseEvent [type ctrl shift alt meta])
 
 (defn mouse-event?
diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs
index 7658de673..bfd704d7d 100644
--- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs
@@ -30,29 +30,16 @@
    [promesa.core :as p]
    [rumext.v2 :as mf]))
 
-(defn strip-position-data [shape]
-  (-> shape
-      (cond-> (some? (meta (:position-data shape)))
-        (with-meta (meta (:position-data shape))))
-      (dissoc :position-data)))
-
-(defn fix-position [shape modifier]
-  (let [shape' (gsh/transform-shape shape modifier)
+(defn fix-position [shape]
+  (let [modifiers (:modifiers shape)
+        shape' (gsh/transform-shape shape modifiers)
         ;; We need to remove the movement because the dynamic modifiers will have move it
         deltav (gpt/to-vec (gpt/point (:selrect shape'))
                            (gpt/point (:selrect shape)))]
     (-> shape
-        (gsh/transform-shape (ctm/move modifier deltav))
-        (mdwm/update-grow-type shape))))
-
-(defn process-shape [modifiers {:keys [id] :as shape}]
-  (let [modifier (dm/get-in modifiers [id :modifiers])]
-    (-> shape
-        (cond-> (and (some? modifier) (not (ctm/only-move? modifier)))
-          (fix-position modifier))
-        (cond-> (nil? (:position-data shape))
-          (assoc :migrate true))
-        strip-position-data)))
+        (gsh/transform-shape (ctm/move modifiers deltav))
+        (mdwm/update-grow-type shape)
+        (dissoc :modifiers))))
 
 (defn- update-with-editor-state
   "Updates the shape with the current state in the editor"
@@ -87,6 +74,7 @@
                    (not (mth/almost-zero? height))
                    (not migrate))
           (st/emit! (dwt/resize-text id width height)))))
+
     (st/emit! (dwt/clean-text-modifier id))))
 
 (defn- update-text-modifier
@@ -136,8 +124,8 @@
   (or (identical? shape other)
       (and
        ;; Check if both shapes are equivalent removing their geometry data
-       (= (dissoc shape :migrate :points :selrect :height :width :x :y)
-          (dissoc other :migrate :points :selrect :height :width :x :y))
+       (= (dissoc shape :migrate :points :selrect :height :width :x :y :position-data :modifiers)
+          (dissoc other :migrate :points :selrect :height :width :x :y :position-data :modifiers))
 
        ;; Check if the position and size is close. If any of these changes the shape has changed
        ;; and if not there is no geometry relevant change
@@ -146,55 +134,69 @@
        (mth/close? (:width shape) (:width other))
        (mth/close? (:height shape) (:height other)))))
 
-(mf/defc viewport-texts-wrapper
-  {::mf/wrap-props false
-   ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
+(mf/defc text-changes-renderer
+  {::mf/wrap-props false}
   [props]
   (let [text-shapes (obj/get props "text-shapes")
-        modifiers (obj/get props "modifiers")
-        prev-modifiers (hooks/use-previous modifiers)
         prev-text-shapes (hooks/use-previous text-shapes)
 
-        ;; A change in position-data won't be a "real" change
         text-change?
         (fn [id]
           (let [new-shape (get text-shapes id)
                 old-shape (get prev-text-shapes id)
-                old-modifiers (ctm/select-geometry (get prev-modifiers id))
-                new-modifiers (ctm/select-geometry (get modifiers id))
-
                 remote? (some? (-> new-shape meta :session-id))]
-            (or (and (not remote?)
+
+            (or (and (not remote?) ;; changes caused by a remote peer are not re-calculated
                      (not (text-properties-equal? old-shape new-shape)))
-
-                (and (not= new-modifiers old-modifiers)
-                     (or (ctm/empty? new-modifiers)
-                         (ctm/empty? old-modifiers)))
-
-                (and (not= new-modifiers old-modifiers)
-                     (or (not (ctm/only-move? new-modifiers))
-                         (not (ctm/only-move? old-modifiers))))
-
                 ;; When the position data is nil we force to recalculate
-                (:migrate new-shape))))
+                (nil? (:position-data new-shape)))))
 
         changed-texts
         (mf/use-memo
-         (mf/deps text-shapes modifiers)
+         (mf/deps text-shapes)
          #(->> (keys text-shapes)
                (filter text-change?)
                (map (d/getf text-shapes))))
 
-        handle-update-modifier (mf/use-callback update-text-modifier)
         handle-update-shape (mf/use-callback update-text-shape)]
 
-    [:*
+    [:.text-changes-renderer
      (for [{:keys [id] :as shape} changed-texts]
-       [:& text-container {:shape shape
-                           :on-update (if (some? (get modifiers (:id shape)))
-                                        handle-update-modifier
-                                        handle-update-shape)
-                           :key (str (dm/str "text-container-" id))}])]))
+       [:& text-container {:key (str (dm/str "text-container-" id))
+                           :shape shape
+                           :on-update handle-update-shape}])]))
+
+(mf/defc text-modifiers-renderer
+  {::mf/wrap-props false}
+  [props]
+  (let [text-shapes (-> (obj/get props "text-shapes")
+                        (update-vals fix-position))
+
+        prev-text-shapes (hooks/use-previous text-shapes)
+
+        text-change?
+        (fn [id]
+          (let [new-shape (get text-shapes id)
+                old-shape (get prev-text-shapes id)]
+            (and
+             (some? new-shape)
+             (some? old-shape)
+             (not (text-properties-equal? old-shape new-shape)))))
+
+        changed-texts
+        (mf/use-memo
+         (mf/deps text-shapes)
+         #(->> (keys text-shapes)
+               (filter text-change?)
+               (map (d/getf text-shapes))))
+
+        handle-update-shape (mf/use-callback update-text-modifier)]
+
+    [:.text-changes-renderer
+     (for [{:keys [id] :as shape} changed-texts]
+       [:& text-container {:key (str (dm/str "text-container-" id))
+                           :shape shape
+                           :on-update handle-update-shape}])]))
 
 (mf/defc viewport-text-editing
   {::mf/wrap-props false}
@@ -256,21 +258,30 @@
         text-shapes
         (mf/use-memo
          (mf/deps objects)
-         #(into {} (filter (comp cph/text-shape? second)) objects))
+         (fn []
+           (into {} (filter (comp cph/text-shape? second)) objects)))
 
         text-shapes
-        (mf/use-memo
-         (mf/deps text-shapes modifiers)
-         #(update-vals text-shapes (partial process-shape modifiers)))
+        (hooks/use-equal-memo text-shapes)
 
         editing-shape (get text-shapes edition)
 
-        ;; This memo is necessary so the viewport-text-wrapper memoize its props correctly
-        text-shapes-wrapper
+        text-shapes-changes
         (mf/use-memo
          (mf/deps text-shapes edition)
          (fn []
-           (dissoc text-shapes edition)))]
+           (-> text-shapes
+               (dissoc edition))))
+
+        text-shapes-modifiers
+        (mf/use-memo
+         (mf/deps modifiers text-shapes)
+         (fn []
+           (into {}
+                 (keep (fn [[id modifiers]]
+                         (when-let [shape (get text-shapes id)]
+                           (vector id (merge shape modifiers)))))
+                 modifiers)))]
 
     ;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are
     ;; edited
@@ -284,5 +295,5 @@
      (when editing-shape
        [:& viewport-text-editing {:shape editing-shape}])
 
-     [:& viewport-texts-wrapper {:text-shapes text-shapes-wrapper
-                                 :modifiers modifiers}]]))
+     [:& text-modifiers-renderer {:text-shapes text-shapes-modifiers}]
+     [:& text-changes-renderer {:text-shapes text-shapes-changes}]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
index 64a25b603..8b6eb7895 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
@@ -28,28 +28,40 @@
         has-value? (not (nil? blur))
         multiple? (= blur :multiple)
 
-        change! (fn [update-fn] (st/emit! (dch/update-shapes ids update-fn)))
+        change!
+        (mf/use-callback
+         (mf/deps ids)
+         (fn [update-fn]
+           (st/emit! (dch/update-shapes ids update-fn))))
 
         handle-add
-        (fn []
-          (change! #(assoc % :blur (create-blur))))
+        (mf/use-callback
+         (mf/deps change!)
+         (fn []
+           (change! #(assoc % :blur (create-blur)))))
 
         handle-delete
-        (fn []
-          (change! #(dissoc % :blur)))
+        (mf/use-callback
+         (mf/deps change!)
+         (fn []
+           (change! #(dissoc % :blur))))
 
         handle-change
-        (fn [value]
-          (change! #(cond-> %
-                      (not (contains? % :blur))
-                      (assoc :blur (create-blur))
+        (mf/use-callback
+         (mf/deps change!)
+         (fn [value]
+           (change! #(cond-> %
+                       (not (contains? % :blur))
+                       (assoc :blur (create-blur))
 
-                      :always
-                      (assoc-in [:blur :value] value))))
+                       :always
+                       (assoc-in [:blur :value] value)))))
 
         handle-toggle-visibility
-        (fn []
-          (change! #(update-in % [:blur :hidden] not)))]
+        (mf/use-callback
+         (mf/deps change!)
+         (fn []
+           (change! #(update-in % [:blur :hidden] not))))]
 
     [:div.element-set
      [:div.element-set-title
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index 81b2ecec9..da5d13c89 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -12,6 +12,7 @@
    [app.common.geom.shapes :as gsh]
    [app.common.pages.helpers :as cph]
    [app.common.types.shape.layout :as ctl]
+   [app.main.data.workspace.modifiers :as dwm]
    [app.main.refs :as refs]
    [app.main.ui.context :as ctx]
    [app.main.ui.hooks :as ui-hooks]
@@ -48,6 +49,20 @@
 
 ;; --- Viewport
 
+(defn apply-modifiers-to-selected
+  [selected objects text-modifiers modifiers]
+  (into []
+        (comp
+         (keep (d/getf objects))
+         (map (fn [{:keys [id] :as shape}]
+                (cond-> shape
+                  (and (cph/text-shape? shape) (contains? text-modifiers id))
+                  (dwm/apply-text-modifier (get text-modifiers id))
+
+                  (contains? modifiers id)
+                  (gsh/transform-shape (dm/get-in modifiers [id :modifiers]))))))
+        selected))
+
 (mf/defc viewport
   [{:keys [wlocal wglobal selected layout file] :as props}]
   (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
@@ -80,6 +95,7 @@
         base-objects      (-> objects (ui-hooks/with-focus-objects focus))
 
         modifiers         (mf/deref refs/workspace-modifiers)
+        text-modifiers    (mf/deref refs/workspace-text-modifier)
 
         objects-modified  (mf/with-memo [base-objects modifiers]
                             (gsh/apply-objects-modifiers base-objects modifiers selected))
@@ -120,7 +136,8 @@
         drawing-tool      (:tool drawing)
         drawing-obj       (:object drawing)
 
-        selected-shapes   (into [] (keep (d/getf objects-modified)) selected)
+        selected-shapes   (apply-modifiers-to-selected selected base-objects text-modifiers modifiers)
+
         selected-frames   (into #{} (map :frame-id) selected-shapes)
 
         ;; Only when we have all the selected shapes in one frame
@@ -303,7 +320,7 @@
                outlined-frame (get objects outlined-frame-id)]
            [:*
             [:& outline/shape-outlines
-             {:objects objects-modified
+             {:objects base-objects
               :hover #{outlined-frame-id}
               :zoom zoom
               :modifiers modifiers}]
@@ -443,25 +460,25 @@
        ;; DEBUG LAYOUT DROP-ZONES
        (when (debug? :layout-drop-zones)
          [:& wvd/debug-drop-zones {:selected-shapes selected-shapes
-                                   :objects objects-modified
+                                   :objects base-objects
                                    :hover-top-frame-id @hover-top-frame-id
                                    :zoom zoom}])
 
        (when (debug? :layout-content-bounds)
          [:& wvd/debug-content-bounds {:selected-shapes selected-shapes
-                                       :objects objects-modified
+                                       :objects base-objects
                                        :hover-top-frame-id @hover-top-frame-id
                                        :zoom zoom}])
 
        (when (debug? :layout-lines)
          [:& wvd/debug-layout-lines {:selected-shapes selected-shapes
-                                     :objects objects-modified
+                                     :objects base-objects
                                      :hover-top-frame-id @hover-top-frame-id
                                      :zoom zoom}])
 
        (when (debug? :parent-bounds)
          [:& wvd/debug-parent-bounds {:selected-shapes selected-shapes
-                                      :objects objects-modified
+                                      :objects base-objects
                                       :hover-top-frame-id @hover-top-frame-id
                                       :zoom zoom}])
 
diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs
index ec0f9e003..a99a57c6c 100644
--- a/frontend/src/app/util/text_svg_position.cljs
+++ b/frontend/src/app/util/text_svg_position.cljs
@@ -63,7 +63,8 @@
   [shape-id]
 
   (when (some? shape-id)
-    (let [text-nodes (dom/query-all (dm/str "#html-text-node-" shape-id " .text-node"))
+    (let [text-nodes (-> (dom/query (dm/fmt "#html-text-node-%" shape-id))
+                         (dom/query-all ".text-node"))
           load-fonts (->> text-nodes (map resolve-font))
 
           process-text-node