Super slow check-boxes

Hi there,

so I made this app for data visualization and it creates loads of checkboxes to show / hide lines, show annotations, and so on. Unfortunately, it looks like the more check-boxes I use in a form, the slower the UI gets.
I recorded a screencast where it takes about 30 seconds for clicks to register:

screencast

Is there anything I can do about this, or do I have to find a way to get what I want without so many widgets?

Thanks for your help.

Hi @lasinludwig,

That is very problematic indeed. I just tried some code to mass-generate checkboxes like this

with st.form("A form"):
    for column in st.columns(4):
        with column:
            for i in range(1000):
                st.checkbox(f"Checkbox {i}", key=f"{i}{column}")
    submit_button = st.form_submit_button("Submit")

But, even with 4000 checkboxes I didn’t see the behavior you were seeing. Do you have any sort of logic happening that depends on the checkbox state? It shouldn’t really matter, since it’s inside a form, but I’m trying to figure out what could be causing that slowness.

If you have a code snippet that demonstrates the issue, that could be helpful.

Hi blackary,

thank you very much for your help. I’m glad that the problem is not the number of boxes. That means I must be doing something wrong - which is good, because then it can be fixed. :blush:

Do you know what happens in the background, when a check box inside a form is clicked? Does streamlit still go through the entire code, just without executing anything or something?

I have some little css-“hacks” in my code to make the color pickers look the way I want them to and to have headings with hover text. I don’t think they are the problem though (removed them in a test with no change).
Not sure if this helps, but here is the function I use to create the check boxes:

@dics.timer()
def display_options_main():
    with st.expander("Anzeigeoptionen", False):

        with st.form("Anzeigeoptionen"):

            # columns
            col_vis_1, col_vis_2, col_fill, col_anno = st.columns(4)

            # change color picker
            st.markdown(
                """
                <style>
                    div.css-1me30nu {           
                        gap: 0.5rem;
                    }
                    div.css-96rroi {
                        display: flex; 
                        flex-direction: row-reverse; 
                        align-items: center; 
                        justify-content: flex-end; 
                        line-height: 1.6; 
                    }
                    div.css-96rroi > label {
                        margin-bottom: 0px;
                        padding-left: 8px;
                        font-size: 1rem; 
                    } 
                    div.css-96rroi > div {
                        height: 20px;
                        width: 20px;
                        vertical-align: middle;
                    } 
                    div.css-96rroi > div > div {
                        height: 20px;
                        width: 20px;
                        padding: 0px;
                        vertical-align: middle;
                    } 
                </style>   
                """,
                unsafe_allow_html=True,
            )

            # Ăśberschriften
            with col_vis_1:
                text_with_hover("Anzeigen", "Linien, die angezeigt werden sollen")
            with col_vis_2:
                text_with_hover("Farbe", "Linienfarbe wählen")
            with col_fill:
                text_with_hover(
                    "FĂĽllen", "Linien, die zur x-Achse ausgefĂĽllt werden sollen"
                )
            with col_anno:
                text_with_hover("Maximum", "Maxima als Anmerkung mit Pfeil")

            # Check Boxes for line visibility, fill and color
            for line in st.session_state["fig_base"].data:
                l_n = line.name
                l_c = line.line.color
                if (
                    len(line.x) > 0
                    and "hline" not in l_n
                    and l_n is not None
                    and l_c is not None
                ):
                    with col_vis_1:
                        st.checkbox(label=l_n, value=True, key="cb_vis_" + l_n)
                    with col_vis_2:
                        st.color_picker(
                            label=l_n,
                            value=l_c,
                            key="cp_" + l_n,
                        )

                    with col_fill:
                        st.checkbox(label=l_n, value=False, key="cb_fill_" + l_n)

            # Check Boxes for annotations
            for anno in [
                anno.name
                for anno in st.session_state["fig_base"].layout.annotations
                if "hline" not in anno.name
            ]:
                with col_anno:
                    st.checkbox(label=anno, value=False, key="cb_anno_" + anno)

            st.markdown("###")
            but_upd_main = st.form_submit_button("Knöpfle")

    st.markdown("###")

    return but_upd_main

Here’s my attempt to make a fully-complete standalone version of your script, and with the amount of data I used, it didn’t show any lag at all. What does disc.timer do?

from dataclasses import dataclass

import streamlit as st


# dummy
def text_with_hover(text, hover):
    st.write(text)


@dataclass
class Line:
    name: str
    line: dict
    x: str


@dataclass
class Annotation:
    name: str


data = {
    "fig_base": {
        "data": [
            Line("line1", {"color": "#000"}, "abc"),
            Line("line2", {"color": "#080"}, "abc"),
            Line("line3", {"color": "#008"}, "abc"),
            Line("line4", {"color": "#880"}, "abc"),
            Line("line5", {"color": "#808"}, "abc"),
            Line("line6", {"color": "#088"}, "abc"),
            Line("line7", {"color": "#888"}, "abc"),
            Line("line8", {"color": "#000"}, "abc"),
            Line("line9", {"color": "#080"}, "abc"),
            Line("line10", {"color": "#008"}, "abc"),
        ],
        "layout": {
            "annotations": [
                Annotation("anno1"),
                Annotation("anno2"),
                Annotation("anno3"),
                Annotation("anno4"),
                Annotation("anno5"),
                Annotation("anno6"),
                Annotation("anno7"),
            ]
        },
    }
}

st.session_state["fig_base"] = data["fig_base"]


def display_options_main():
    with st.expander("Anzeigeoptionen", False):

        with st.form("Anzeigeoptionen"):

            # columns
            col_vis_1, col_vis_2, col_fill, col_anno = st.columns(4)

            # change color picker
            st.markdown(
                """
                <style>
                    div.css-1me30nu {
                        gap: 0.5rem;
                    }
                    div.css-96rroi {
                        display: flex;
                        flex-direction: row-reverse;
                        align-items: center;
                        justify-content: flex-end;
                        line-height: 1.6;
                    }
                    div.css-96rroi > label {
                        margin-bottom: 0px;
                        padding-left: 8px;
                        font-size: 1rem;
                    }
                    div.css-96rroi > div {
                        height: 20px;
                        width: 20px;
                        vertical-align: middle;
                    }
                    div.css-96rroi > div > div {
                        height: 20px;
                        width: 20px;
                        padding: 0px;
                        vertical-align: middle;
                    }
                </style>
                """,
                unsafe_allow_html=True,
            )

            # Ăśberschriften
            with col_vis_1:
                text_with_hover("Anzeigen", "Linien, die angezeigt werden sollen")
            with col_vis_2:
                text_with_hover("Farbe", "Linienfarbe wählen")
            with col_fill:
                text_with_hover(
                    "FĂĽllen", "Linien, die zur x-Achse ausgefĂĽllt werden sollen"
                )
            with col_anno:
                text_with_hover("Maximum", "Maxima als Anmerkung mit Pfeil")

            # Check Boxes for line visibility, fill and color
            for line in st.session_state["fig_base"]["data"]:
                l_n = line.name
                l_c = line.line["color"]
                if (
                    len(line.x) > 0
                    and "hline" not in l_n
                    and l_n is not None
                    and l_c is not None
                ):
                    with col_vis_1:
                        st.checkbox(label=l_n, value=True, key="cb_vis_" + l_n)
                    with col_vis_2:
                        st.color_picker(
                            label=l_n,
                            value=l_c,
                            key="cp_" + l_n,
                        )

                    with col_fill:
                        st.checkbox(label=l_n, value=False, key="cb_fill_" + l_n)

            # Check Boxes for annotations
            for anno in [
                anno.name
                for anno in st.session_state["fig_base"]["layout"]["annotations"]
                if "hline" not in anno.name
            ]:
                with col_anno:
                    st.checkbox(label=anno, value=False, key="cb_anno_" + anno)

            st.markdown("###")
            but_upd_main = st.form_submit_button("Knöpfle")

    st.markdown("###")

    return but_upd_main


display_options_main()

Thanks for trying this. I think the problem is not with the way the check boxes are created - probably nothing to do with the check boxes at all.
This project is a bit of a mess by now. I’m not a very experienced programmer and every time I learn something new, I try to shove it in this thing. :rofl:

@dics.timer() is a good example. I learned about decorators and made one in this project, that logs the time it takes a function to run. It looks like this:

def timer():
    def decorator(func):
        def wrapper(*args, **kwargs):
            start_time = time.perf_counter()
            if "dic_exe_time" not in st.session_state:
                st.session_state["dic_exe_time"] = {}
            result = func(*args, **kwargs)
            st.session_state["dic_exe_time"][func.__name__] = (
                time.perf_counter() - start_time
            )
            return result

        return wrapper

    return decorator

Unfortunately it doesn’t help me to find the problem with the slow UI since no function is running while I click stuff in the form.

I think the problem is somewhere else. I’ll try to find it and post it here if I do.
Any tips what could cause something like this?

I definitely understand what it’s like to have a project scale out of control :slight_smile:

I don’t have any great ideas off the top of my head, since one of the benefits of putting things in forms is that they shouldn’t make anything happen (i.e. no app reruns) until you actually hit the submit button. So, this seems quite strange.

My general recommendation for debugging something like this is to try and isolate the problem by either:

  1. Removing pieces one at a time until you no longer see the problem (e.g. remove the timer code, separate this page into a standalone app, remove the other tab, if you’ve removed everything else start shrinking the number of checkboxes, etc.)
  2. Do the reverse, start with the main piece (form with checkboxes), and add more and more pieces till you find where it starts slowing down.

The ideal case is that you come up with two different code snippets, and can say “this one works fine, but if I add this extra thing, it starts being slow”. That can be tremendously challenging, but is often the best way to get the bottom of an issue, and either help you solve it, or help someone on our end fix the bug.

Please post again if you have any success, especially if you get a small bit of stand-alone code that reproduces the issue. This looks like a serious issue, but it’s hard to be sure what’s behind it because it’s part of a bigger app.

1 Like

I haven’t found the issue yet, but I’ve noticed that the amount of data makes a difference. In the screencast, the app uses data from a pandas data frame with about 700,000 data points. When I use a data frame with around 2,000 data points, the UI is significantly more responsive. (The search for the issue continues though, because even with the tiny data frame, it’s not as responsive as expected.)

Do you maybe have any tips for handling larger data frames in streamlit? Could this be a resources-issue? Are there any limits of how big the st.session_state dictionary can be? Maybe I put too much stuff in there…

(btw, plotly, which I use for the graphs, has no problems with this amount of data and the interactive charts are very responsive, so I don’t think those are the issue either.)

That is interesting. You may be running into the resource limitations of what is offered with Streamlit Cloud. Do you notice the same issue when running locally, or only when it’s deployed on Streamlit Cloud?

I have the issue locally as well, so it shouldn’t be a problem with the cloud…

Since this is happening only in forms, I don’t think it would be a session_state issue. Session state should be able to handle lots of data easily, since it is implemented as some regular python dicts storing values and metadata.

Since I don’t have your full code, I can’t tell how dataframes tie into the checkbox form code. My initial guess from the screencast is that either the frontend is doing some very slow dataframe computation and blocking the rerendering, or that the rendering itself has some pathological performance issue. I would probably need to do some profiling to determine more.

If you could share some minimal reproduction code, I’d be happy to investigate what’s going on. If not, I can at least put this together in a github issue to keep track of it, since it seems like a severe problem.

I just noticed this issue thread, which is also about poor responsiveness of widgets in forms when there is a plotly chart on the page. Seems related, and suggests something about our plotly chart rendering causing performance problems.

Thank you very much for your reply @AnOctopus. It looks like you found the issue. I just tested it and the plotly plots seem to be the problem. When I simply hide the graphs on the page, all the check boxes and other widgets work normally.

This is very unfortunate for me, as the whole point of my app is to show pretty plotly graphs and there doesn’t seem to be an easy fix. I guess I’ll have to learn how to use Altair or something…

Thanks again for your help.

It is a severe enough issue that I expect to look into it this week, so it will hopefully be fixed in 1.13.1 or 1.14 (a bit late to get it into 1.13.0).

2 Likes