bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
38.61k stars 1.31k forks source link

Memory leak (chromium based browsers only) #2927

Closed varunbpatil closed 2 months ago

varunbpatil commented 2 months ago

Minimal reproducible example (golang)

package main

import (
    "fmt"
    "math/rand"

    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/", home)
    e.GET("/test", data)
    _ = e.Start(":8080")
}

func home(c echo.Context) error {
    return c.HTML(200, `
        <html lang="en">
        <head>
            <meta charset="UTF-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
            <script src="https://unpkg.com/htmx.org@2.0.2" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>
            <title>HTMX</title>
        </head>
        <body>
            <input
                id="search"
                type="search"
                hx-get="/test"
                hx-trigger="keyup changed delay:300ms"
                hx-sync="this:replace"
                hx-target="#search-results"
            />
            <div id="search-results" style="margin-top: 12px"></div>
        </body>
    </html>

    `)
}

func data(c echo.Context) error {
    return c.HTML(200, fmt.Sprintf(`
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Age</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>John Doe</td>
                    <td>%[1]d</td>
                </tr>
                <tr>
                    <td>John Doe</td>
                    <td>%[1]d</td>
                </tr>
            </tbody>
        </table>
    `, rand.Intn(100)))
}

This is a very simple search app. An input field makes a GET request to /test which returns a table as the response. The request is made automatically on keyup with a delay of 300ms.

The memory leak

If I am simply modifying the input field and not interacting with the search results, there is no memory leak. But, the moment I select some text in the result using the mouse and then make a new search request, the <table> and all it's children become detached nodes which cannot be garbage collected.

This is a small app, so the memory leak is not obvious, but is still visible in the devtools "memory" tab. When I take a heap snapshot after manually running GC, I can see several detached nodes.

On a real search app, where the results contain many nodes, this quickly blows up when clicking/selecting the search results.

Here is a recording of the issue.

https://github.com/user-attachments/assets/756d78a4-0eff-4147-86c6-577aefc8aacb

My investigation so far

  1. Noticed that this doesn't happen on Firefox.
  2. There are some chromium issues around input focus and memory leaks - https://issues.chromium.org/issues/342247579, but I'm not sure if it applies to table rows like in this example.
  3. I am not a frontend developer, so I don't know how to debug this memory leak. I can help get more information if I'm guided.

Why am I posting on HTMX github?

This is probably very much a chromium issue. But, I'm surprised that not many people have encountered it. I'm mostly looking for half decent workarounds in HTMX if possible so that my app is usable on chromium based browsers.

Versions

Chromium: 129.0.6668.59 HTMX: 2.0.2

croxton commented 2 months ago

Fascinating. Could you please try this test again but put the input and the #search-results inside separate divs, so they don't share the same parent element? I'm curious if only the input becomes detached in that scenario.

My reasoning: if Chrome stores the parent element of a focussed input in memory, that would include #search-results. So the workaround would be to isolate the input.

varunbpatil commented 2 months ago

Hi @croxton , I just tried this but it did not work. Still see the detached nodes.

        <body>
            <div>
                <input
                    id="search"
                    type="text"
                    hx-get="/test"
                    hx-trigger="keyup changed delay:300ms"
                    hx-sync="this:replace"
                    hx-target="#search-results"
                />
            </div>
            <div>
                <div id="search-results" style="margin-top: 12px"></div>
            </div>
        </body>
croxton commented 2 months ago

Hmm. Does it still happen if you remove the hx-sync="this:replace" attribute on the input?

varunbpatil commented 2 months ago

Yes, tried that just now. Still happening.

varunbpatil commented 2 months ago

Also removed the hx-trigger. Thought maybe some event handler is holding a reference or something. But, still seeing detached nodes.

varunbpatil commented 2 months ago

Oh shoot. It is the devtools itself which is holding a reference !!. If I don't have devtools open when I click on the table, there is no memory leak.

croxton commented 2 months ago

Ha brilliant! Well that's great news because that would have been a showstopper.

varunbpatil commented 2 months ago

Sorry for the wild goose chase @croxton and thanks for the help.