reagent 2024-11-22

Hi, I’m hoping someone can help shed some light on what might be happening here. I have a large memory leak in my app that I think I’ve narrowed down to a mouse-down handler which rerenders the sub component tree. If I use on-mouse-down then I get a leak, but the same code with a on-click handler does not leak. I think the whole sub tree is being kept around, but it’s very noticeable if the arguments to one of the sub functions is a very large object. I am on reagent 1.1.1 The code below will leak about 100mb every time you click back and forth between the components

(defn create-large-object []
  (into-array
   (mapv
    (fn [i]
      {:id i
       :s  (str i)})
    (range 0 500000))))

(defn leak-memeory-sub-comp [props]
  [:div "I'm leaking"])

(defn leak-memory-comp []
  (r/with-let [large-object (create-large-object)]
    [leak-memeory-sub-comp {:large-object large-object}]))

(defn memory-leak-main []
  (r/with-let [*focused? (r/atom false)]
    [:div
     {:style {:padding-top "24px"
              :text-align "center"
              :font-size "36px"}}
     [:div
      ;; on-mouse-down causes it to leak memory from the sub components argv
      {:on-mouse-down #(swap! *focused? not)}
      ;; but on-click does not
      ;; {:on-click #(swap! *focused? not)}
      (if @*focused?
        [leak-memory-comp]
        [:div "click me!"])]]))

100MB? Sounds a lot for half a million of clojure maps how do you know it's a memory leak?

By leak I mean the memory keeps increasing every time I click the div twice. forcing garbage collection does not clear the memory I would think that, if “click me” is showing that the large object shouldn’t be in memory, but it is. Here is the object in a heap snapshot

possibly the 100MB could be because keywords are not interned unless it’s advanced?

🤔 1

I see, that's interesting find. Would be good to verify that it's Reagent's problem, not React itself.

I don't know Reagent's internals well, but I think the problem might be here https://github.com/search?q=repo%3Areagent-project%2Freagent%20generation&type=code and if it is, then it should be dev only issue, so I'd also check of the same leak occurs in prod builds

on the other hand generation is just a number, does the leak happen when using form-2 components instead of with-let?

In the production build of my app I have the same memory leak, but there is a lot more going on so could be from something else, I will check form-2 and try a production build

it seems to still leak with form-2

is that an isolated example? I'm interested to debug it as well

❤️ 1

yeah that example should leak for you, I am not in a fresh project but I pasted it at the top of my component tree

👍 1

It is also leaking in a production build for me

interesting that the on-click handler fixes it because in my app, the leak actually happens without clicking on anything, I trigger the leak from key downs, still haven’t connected if the leak in my example and my app are the same

I will make a fresh project so this is easier to reproduce

Hmm, I'm not seeing a leak reagent 1.1.1, react 18

oh I am on react 17

is that Chrome browser of Electron?

I will try this in a fresh project and see if i can reproduce

react “^17.0.2”

> oh I am on react 17 what is the exact React version?

thanks

hehe, seems like it is React's issue

I get it in v17 but not in v18

maybe not React's fault, could be also something with how Reagent interacts with React, since v18 is a bit different

it should be safe to upgrade to v18 btw

aren’t there breaking changes? I have a massive list of dependencies that haven’t been upgraded and extensions built on top of my app the rely on react 17 lol

in v18 if you still use rdom/render instead of React Root API, React will behave as if it's v17, with minor differences but I'd keep an eye on those and do a bit of manual testing to make sure nothing weird is going on

At the time at Pitch upgrading from v17 to v18 in "legacy" mode went w/o any major problems, it's upgrading to concurrent rendering mode (using react root) that makes a big difference

👍 1

Is there a replacement for dom-node? I think we use this a lot

well, if it’s the only way to solve this leak then I will have to upgrade

not sure but I don't think those APIs were actually removed

found this in v18.0.0 changelog

that makes it sound like there is also an issue in reagent?

Thanks for your help!

I will open an issue in reagent

For anyone else that comes across this thread, here is a repo you can reproduce it with https://github.com/panterarocks49/reagent-memory-leak

ok, tested in plain React v17, still leaking, pretty sure now that it's React's problem

❤️ 1

it does GC leaked values at some point though ,just that when using JS objects 500k of them is only 10-15MB of memory so it's not very visible

but when using Clojure maps, for some reason GC won't kick in, still waiting

sorry, do you mean it does GC javascript maps but not clojure maps?

could you post the react code?

> sorry, do you mean it does GC javascript maps but not clojure maps? correct

(ns example.core
  (:require ["react-dom" :as rdom]
            [react]))

(def $ react/createElement)

(defn create-large-object []
  (into-array
   (mapv
    (fn [i]
      #js {:id i
           :s  (str i)})
    (range 0 500000))))

(defn leak-memeory-sub-comp [props]
  ($ "div" nil "I'm leaking"))

(defn leak-memory-comp []
  (let [large-object (create-large-object)]
    ($ leak-memeory-sub-comp #js {:argv {:largeObject large-object}})))

(defn memory-leak-main []
  (let [[focused? set-focused] (react/useState false)]
    ($ "div"
     #js {:style #js {:paddingTop "24px"
                      :textAlign "center"
                      :fontAize "36px"}}
     ($ "div"
        ;; on-mouse-down causes it to leak memory from the sub components argv
        #js {:onMouseDown #(set-focused not)}
        ;; but on-click does not
        ;; {:on-click #(swap! *focused? not)}
        (if focused?
          ($ leak-memory-comp)
          ($ "div" nil "click me!"))))))

(.render rdom
  ($ memory-leak-main)
  (js/document.getElementById "root"))

the thing that hangs in memory is something called memoizedProps, that's basically current props tracked by react internally the field is attached to fiber instance that's referenced in a DOM node that corresponds to a component that takes that big value

so it's not with-let, but just the fact that values are passed in props

ahh, I wonder why it is cleaned up correctly with on-click as opposed to mouse down

I can confirm react 18 solves the issue in my full app.. hopefully upgrading is not too painful

👍 1