Skip to content

Conversation

@evnchn
Copy link
Collaborator

@evnchn evnchn commented Oct 19, 2025

Motivation

Observing how we basically re-import NiceGUI again if we want to do run.cpu_bound, lazy imports which defer heavy imports until actually needed is a promising approach to make it faster (ref: #5684)

Implementation Strategy

It began with my rapid iteration with Manus to use https://github.com/bachorp/lazy-imports, and we iterate until from nicegui import ui works.

Implementation (this part by Copilot)

Refactoring and performance improvements:

Clipboard API modernization:

UI lazy loading:

  • Introduced lazy imports for all UI elements in ui.py using the lazy_imports library, significantly improving startup performance by loading modules only when accessed. Registered the lazy module in sys.modules for proper caching. [1] [2] [3] [4]
  • Added lazy-imports as a required dependency in pyproject.toml.

Import handling fixes:

Progress

  • I chose a meaningful title that completes the sentence: "If applied, this PR will..."
  • The implementation is complete.
  • Pytests pending: Not sure if Pytest test isolation ensure each test start off fresh, or if some components are already lazily-loaded. Either way we want to test both if possible
  • Documentation is still good-to-have since this lazy-import behaviour could throw off some people, for example those who expect the library to currently function in a certain way.

@evnchn evnchn marked this pull request as draft October 19, 2025 14:45
@evnchn

This comment was marked as outdated.

@evnchn

This comment was marked as resolved.

@evnchn
Copy link
Collaborator Author

evnchn commented Oct 19, 2025

f415575 fixed the double-access on ui.label breaking.

Now the basic code works:

from nicegui import ui

ui.label('First label')
ui.label('Second label. Did not break!')

ui.run()

As always pipeline will be our guiding star. Let's see tomorrow.

@evnchn
Copy link
Collaborator Author

evnchn commented Oct 19, 2025

Pipeline's less dramatic than I think. 2 things are broken, mainly:

  1. ui.clipboard totally unavailable (I think this is truly what the docs warned us)
  2. importmap is empty from the start, and so ui.mermaid and ui.echarts does not work.

Actually point 2 can theoretically affect someone who's

  • creating custom elements,
  • expect to use importmap for importing your stuff,
  • and 🤡 enough to dynamically define the element's class.

Gist is: with lazy-imports what we call 🤡 before is actually the 🧠 move empowering lazy import's efficiency.

@evnchn evnchn force-pushed the feature/lazy-imports branch from 801bf46 to 425cb4f Compare October 19, 2025 19:44
@evnchn
Copy link
Collaborator Author

evnchn commented Oct 19, 2025

Well, as of 425cb4f the only failing tests is the ui.clipboard one.

@falkoschindler This is getting somewhere, somewhere farther than anywhere we had been. Consider closing:

@evnchn
Copy link
Collaborator Author

evnchn commented Oct 19, 2025

If in #4564 we swapped a function for a Class, may we in this case swap a module for a Class and then we can solve issue 1.

@falkoschindler
Copy link
Contributor

@evnchn I added the other PRs to the description of this PR. If this PR converges and is merged eventually, we can close them.

@evnchn
Copy link
Collaborator Author

evnchn commented Oct 20, 2025

Actually this PR is more mergable if we incorporate https://github.com/evnchn/nicegui/tree/feature/lazy-imports-clipboard-class @falkoschindler can you review the changes there and then if it looks good we can merge that into this PR

@evnchn
Copy link
Collaborator Author

evnchn commented Oct 20, 2025

Crazily efficient 💯

image

Running:

from nicegui import ui


@ui.page('/')
def page():
    ui.highchart({'series': {'data': [1.0]}}, type='stockChart', extras=['stock'])


ui.run(show=False, reload=False)

@evnchn evnchn added the feature Type/scope: New or intentionally changed behavior label Oct 20, 2025
@falkoschindler
Copy link
Contributor

@evnchn What's the status of this PR?
As I just described in #5684, it's currently our best bet to improve the initialization of multiprocesses.

@falkoschindler falkoschindler added this to the 3.x milestone Jan 26, 2026
…ance

- Add lazy-imports dependency to pyproject.toml
- Convert ui.py to use LazyModule for deferred loading of UI elements
- Achieve 85% reduction in import time (from ~4.3s to ~0.66s)
- Maintain full backward compatibility with existing API
- Preserve TYPE_CHECKING branch for static type checkers

The lazy loading implementation defers the import of heavy UI element
classes until they are first accessed, significantly improving the
initial import time of NiceGUI while maintaining all functionality.
The previous implementation using the __getattr__ pattern had a critical
bug where accessing the same attribute twice would fail with AttributeError.

Root cause: When using __getattr__ pattern, the LazyModule instance is not
registered in sys.modules. When LazyModule.__getattr__ calls setattr(self, name, value),
it sets the attribute on the LazyModule instance, not on the actual module
in sys.modules. On second access, the attribute is no longer in __deferred_attrs
(it was popped), but it's also not on the module, causing AttributeError.

Solution: Use load(_mod) to register the LazyModule instance in sys.modules,
ensuring setattr() actually sets attributes on the module Python sees.

This fix:
- Resolves the double-access bug
- Maintains the same performance (610ms vs 655ms - actually slightly better)
- Passes all comprehensive tests including multiple re-accesses
- Properly caches accessed attributes on the module
@evnchn evnchn force-pushed the feature/lazy-imports branch from 5de423b to 231bacc Compare January 26, 2026 15:02
@evnchn evnchn force-pushed the feature/lazy-imports branch from 231bacc to e720b60 Compare January 26, 2026 15:06
@evnchn
Copy link
Collaborator Author

evnchn commented Jan 26, 2026

@falkoschindler I chose to rebase because the merge conflict is unreal. Also nobody remember what happened those 98 days ago, so rebase over merge is more similar to how our memory works 🧠

Nevertheless, I want to report: Despite what the original description says, this PR is working!

test.py (frankly you can put whatever)

from nicegui import ui

@ui.page('/')
def page():
    ui.json_editor({})

ui.run(show=False, reload=False)

time_script.sh

#!/bin/bash

source /workspaces/nicegui/.venv/bin/activate

start=$(date +%s.%N)

coproc python test.py

while read -r line <&${COPROC[0]}; do
    echo "$line"
    if [[ "$line" == *"NiceGUI ready to go on"* ]]; then
        end=$(date +%s.%N)
        elapsed=$(awk "BEGIN {print $end - $start}")
        echo "Time taken: $elapsed seconds"
        kill $COPROC_PID
        break
    fi
done
  • Before the PR: 0.6 seconds
  • After this PR: 0.35 seconds

@evnchn
Copy link
Collaborator Author

evnchn commented Jan 26, 2026

Would consider that this is not draft. If you agree, you can mark it ready for review and review it.

@falkoschindler falkoschindler modified the milestones: 3.x, 3.7 Jan 27, 2026
@falkoschindler falkoschindler added the review Status: PR is open and needs review label Jan 27, 2026
@falkoschindler falkoschindler marked this pull request as ready for review January 27, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Type/scope: New or intentionally changed behavior review Status: PR is open and needs review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slow startup and code re-execution in multiprocessing contexts

2 participants