Pn.widgets.*.from_param is very slow to instantiate

I was following the doc’s here Create Custom Widget to try to build some custom widgets that i didn’t thing were overly complicated. But, i’m having real issues with performance. I’m trying to build widgets that a user can instantiate and add to a display dynamically. One of these has about 4 parameters and panel widgets that are instantiated with .from_param() but it seems like each of those .from_param() calls takes about 100ms to complete, it then takes a further 1/2 second to add that widget to the display. So for adding a real simple little UI element with 4 inputs it takes a full second for the UI to respond.

What am i doing wrong? It seems like it should be fast, in my example i also instantiated the panel objects themselves which is quick, tho still very slow to add to the display (and doesn’t have any reactivity configured).

Here’s a test script with some simple logging to measure some timing.

import datetime
import logging

import panel as pn
import param
from panel.custom import PyComponent
from panel.widgets.base import WidgetBase

# Create module-specific logger.
_LOGGER = logging.getLogger(__name__)
_LOGGER.propagate = False  # Prevent messages from bubbling up to the root logger
_LOGGER.setLevel(logging.DEBUG)

# Clear any existing handlers (e.g. from panel or other libraries)
if _LOGGER.hasHandlers():
    _LOGGER.handlers.clear()

formatter = logging.Formatter(
    "%(asctime)s.%(msecs)03d %(levelname)s {%(module)s} [%(funcName)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
_LOGGER.addHandler(handler)


class SlowTest(WidgetBase, PyComponent):
    start = param.Integer(doc="Start")
    end = param.Number(doc="End")
    opts = param.Selector(
        default="a",
        objects=["a", "b", "c"],
        doc="Options",
    )
    test = param.String(doc="Test")

    def __init__(self, **params):
        tic = datetime.datetime.now()
        super().__init__(**params)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Init took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()

        self.start_disp = pn.widgets.IntInput.from_param(self.param.start)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Start took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()
        self.end_disp = pn.widgets.FloatInput.from_param(self.param.end)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("End took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()

        self.opts_disp = pn.widgets.Select.from_param(self.param.opts)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Opts took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()

        self.test_disp = pn.widgets.TextInput.from_param(self.param.test)

        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Test took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()
        self._display = pn.Column(
            self.start_disp,
            self.end_disp,
            self.opts_disp,
            self.test_disp,
        )
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Display took: %.3f seconds", toc.total_seconds())

    def __panel__(self):
        return self._display


class FastTest(WidgetBase, PyComponent):
    start = param.Integer(doc="Start")
    end = param.Number(doc="End")
    opts = param.Selector(
        default="a",
        objects=["a", "b", "c"],
        doc="Options",
    )
    test = param.String(doc="Test")

    def __init__(self, **params):
        tic = datetime.datetime.now()
        super().__init__(**params)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Init took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()

        self.start_disp = pn.widgets.IntInput(value=self.start)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Start took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()
        self.end_disp = pn.widgets.FloatInput(value=self.end)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("End took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()

        self.opts_disp = pn.widgets.Select(value=self.opts)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Opts took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()

        self.test_disp = pn.widgets.TextInput(value=self.test)

        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Test took: %.3f seconds", toc.total_seconds())
        tic = datetime.datetime.now()
        self._display = pn.Column(
            self.start_disp,
            self.end_disp,
            self.opts_disp,
            self.test_disp,
        )
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Display took: %.3f seconds", toc.total_seconds())

    def __panel__(self):
        return self._display


class TestApp(WidgetBase, PyComponent):
    tests = param.List(doc="Tests")

    def __init__(self, **params):
        super().__init__(**params)
        self.add_button = pn.widgets.Button(name="Add SlowTest")
        self.add_button.on_click(self.add_test)
        self.add_button_fast = pn.widgets.Button(name="Add FastTest")
        self.add_button_fast.on_click(self.add_test_fast)
        self._display = pn.Column(
            pn.Row(self.add_button, self.add_button_fast),
            *self.tests,
        )

    def add_test(self, event):
        tic = datetime.datetime.now()
        new_test = SlowTest()
        self.tests.append(new_test)
        self._display.append(new_test)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Add Test took: %.3f seconds", toc.total_seconds())

    def add_test_fast(self, event):
        tic = datetime.datetime.now()
        new_test = FastTest()
        self.tests.append(new_test)
        self._display.append(new_test)
        toc = datetime.datetime.now() - tic
        _LOGGER.debug("Add Test took: %.3f seconds", toc.total_seconds())

    def __panel__(self):
        return self._display


tic = datetime.datetime.now()
TestApp().servable()
toc = datetime.datetime.now() - tic
_LOGGER.debug("Initial Render took: %.3f seconds", toc.total_seconds())
2025-03-03 10:58:07.783 DEBUG {test_panel} [__init__] Fast Init took: 0.001 seconds
2025-03-03 10:58:07.784 DEBUG {test_panel} [__init__] Fast Start took: 0.001 seconds
2025-03-03 10:58:07.786 DEBUG {test_panel} [__init__] Fast End took: 0.001 seconds
2025-03-03 10:58:07.792 DEBUG {test_panel} [__init__] Fast Opts took: 0.006 seconds
2025-03-03 10:58:07.794 DEBUG {test_panel} [__init__] Fast Test took: 0.001 seconds
2025-03-03 10:58:07.818 DEBUG {test_panel} [__init__] Display took: 0.024 seconds
2025-03-03 10:58:08.225 DEBUG {test_panel} [add_test_fast] Add Fast Test took: 0.443 seconds
2025-03-03 10:58:09.186 DEBUG {test_panel} [__init__] Init took: 0.000 seconds
2025-03-03 10:58:09.286 DEBUG {test_panel} [__init__] Start took: 0.100 seconds
2025-03-03 10:58:09.383 DEBUG {test_panel} [__init__] End took: 0.097 seconds
2025-03-03 10:58:09.483 DEBUG {test_panel} [__init__] Opts took: 0.100 seconds
2025-03-03 10:58:09.578 DEBUG {test_panel} [__init__] Test took: 0.094 seconds
2025-03-03 10:58:09.601 DEBUG {test_panel} [__init__] Display took: 0.022 seconds
2025-03-03 10:58:10.176 DEBUG {test_panel} [add_test] Add Slow Test took: 0.990 seconds

I think your usage looks fine, but feel free to raise a Panel GitHub issue with the same contents as this.

Thanks for the follow up. New issue here #7756.

No problem! Thanks for filing the issue.

One other thought is profiling your application and creating a viz out of it; I think the other maintainers would appreciate that!
https://panel.holoviz.org/how_to/profiling/profile.html

Hi @ahuang11 . When you say the usage seems fine, do you mean the usage in the code, or the performance that is reported?

The Github issue mentions 0.6 seconds for widget creation; that seems to be worryingly slow?

I mean the code looks correct, and that’d be how I would I write it.

I’m also unsure if 0.6 seconds is too slow.

The Fast and Slow are not producing the same widgets as far as I can tell.
The .from_param add additional information to the widget based on the parameter definition, eg.

  • boundaries (but you haven’t any set in the param.Integer or param.Number)
  • the Select widget doesn’t have any values to select from in your FAST example
  • Help

regards

PS: My test-runs took around 20-40ms, fluctuate a bit on reruns, and yes, the from_param takes longer (not unexpected because the widgets contain more information/structure)

1 Like