Skip to content

Conversation

@evnchn
Copy link
Collaborator

@evnchn evnchn commented Nov 23, 2025

Motivation

TL-DR: dependency.py isn't coded as tightly as it should, silently letting (possibly critical) errors slide into client-side instead of stopping them right away and preventing the server from launching in a verbose and attention-grabbing manner.

I was working on #5493 and looking for space savings, when I found some interesting behaviour with regards to our dependency.py implementation.

  1. We set key for imports at 2 places, libraries and ESM modules

imports[library.name] = f'{prefix}/_nicegui/{__version__}/libraries/{key}'
done_libraries.add(key)
# build the importmap structure for ESM modules
for key, esm_module in esm_modules.items():
imports[f'{esm_module.name}'] = f'{prefix}/_nicegui/{__version__}/esm/{key}/index.js'
imports[f'{esm_module.name}/'] = f'{prefix}/_nicegui/{__version__}/esm/{key}/'

If ESM modules are to have the same name as a library, the library would be shadowed.

  1. We append JS imports into js_imports at 2 places, one for .js components, one for .vue components

js_imports.append(f'import {{ default as {vue_component.name} }} from "{url}";')
js_imports.append(f"{vue_component.name}.template = '#tpl-{vue_component.name}';")
js_imports.append(f'app.component("{vue_component.tag}", {vue_component.name});')
vue_styles.append(vue_component.style)
done_components.add(key)
# build the resources associated with the elements
for element in elements:
if element.component:
js_component = element.component
if js_component.key not in done_components and js_component.path.suffix.lower() == '.js':
url = f'{prefix}/_nicegui/{__version__}/components/{js_component.key}'
js_imports.append(f'import {{ default as {js_component.name} }} from "{url}";')
js_imports.append(f'app.component("{js_component.tag}", {js_component.name});')
js_imports_urls.append(url)

This one is a bit more serious: If you run import twice with same name, browser JS errors out entirely, and the page grinds to a halt (white screen)

Uncaught SyntaxError: Identifier 'BLAH' has already been declared

Implementation

  • 2 sets: import_names and component_names
  • Do assertations before registration
  • Update the set before returning

Progress

  • I chose a meaningful title that completes the sentence: "If applied, this PR will..."
  • The implementation is complete.
  • Pytests have been added (or are not necessary).
  • Documentation has been added (or is not necessary).

Final notes

May break existing code which works (barely) in case 1, but I think we should break it, because the code isn't valid in the first place.

@evnchn
Copy link
Collaborator Author

evnchn commented Nov 23, 2025

And if we unique-key by name, instead of unique-key by hash-based key, we get maximum savings in the following image (hence why I thought of this when working on #5493:

image

@evnchn evnchn added the bug Type/scope: Incorrect behavior in existing functionality label Nov 23, 2025
@falkoschindler falkoschindler added the review Status: PR is open and needs review label Nov 24, 2025
@falkoschindler falkoschindler added this to the 3.5 milestone Nov 24, 2025
@evnchn
Copy link
Collaborator Author

evnchn commented Nov 24, 2025

No I think we should test this, in retrospect.

@falkoschindler
Copy link
Contributor

No I think we should test this, in retrospect.

@evnchn Are you planning to write tests for this PR? To be honest, I'm not sure how to do it without too much effort...

Another question: Should we cross-check import and component names?

# In register_vue_component (lines 91 and 100):
assert name not in component_names, f'Component name "{name}" is already used'
assert name not in import_names, f'Component name "{name}" conflicts with library/ESM module name'

# In register_library (line 115):
assert name not in import_names, f'Library / ESM module name "{name}" is already used'
assert name not in component_names, f'Library / ESM module name "{name}" conflicts with component name'

# In register_esm (line 141):
assert name not in import_names, f'Library / ESM module name "{name}" is already used'
assert name not in component_names, f'ESM module name "{name}" conflicts with component name'

@evnchn
Copy link
Collaborator Author

evnchn commented Dec 15, 2025

Are you planning to write tests for this PR?

Ideally we should, but 3.4.1/3.5 is round the table, and I am kinda busy until 20th, so I am not sure. Maybe we can skip that for now?

Cross-check the names

Actually yes. Consider:

js_imports.append(f'import {{ default as {vue_component.name} }} from "{url}";')
js_imports.append(f'import {{ default as {js_component.name} }} from "{url}";')

Collision in name is lethal. So we should error out in a cross-checked manner.


But I also have an interesting thought: Rather than erroring out, can NiceGUI dodge conflicts when they arise?

Seeing that we already imported components under a different name in #5562 (import { default as keyboard } from ... becomes import { pack_keyboard } from ...)

But how much do we want to endorse this? I feel like we shouldn't dodge for the user, if (1) it is bad behaviour in the first place, (2) right now the code doesn't work so why make it work and expand the API capabilities.

@falkoschindler falkoschindler modified the milestones: 3.5, 3.4.1 Dec 16, 2025
@evnchn
Copy link
Collaborator Author

evnchn commented Dec 16, 2025

While code is clean, the perf may not be good if the set is recreated every time the check occurs.

The old approach of maintaining a set may be still good, if we pack that into _name_is_unique

@evnchn
Copy link
Collaborator Author

evnchn commented Dec 16, 2025

I inspected the set we are testing against:

def _name_is_unique(name: str) -> bool:
    esm_names = {esm_module.name for esm_module in esm_modules.values()}
    import os
    os.system('clear')
    print({'vue', 'sass', 'immutable', *vue_components, *js_components, *libraries, *esm_names})
    return name not in {'vue', 'sass', 'immutable', *vue_components, *js_components, *libraries, *esm_names}

And got the following results:

{'2f7a651a8fc4cd0b6935da8eb1c1586e/timer.js', 'nicegui-plotly', 'cf39ebe9b6a4677e62cb4f6091f0f034/echart.js', 'e8a7936cfd9e7babc768bb5abc968af4/interactive_image.js', '9048d917db8394f1812d60599c4e696d/full-screen.js', '451dfdeffc47b5cc4d420275a874bbb3/header.js', 'e010e7402bba2f60f049b2e3111c7e17/table.js', '9048d917db8394f1812d60599c4e696d/dependency-wheel.js', 'ee5c14fe887a8783e4186e989f559725/query.js', '9048d917db8394f1812d60599c4e696d/current-date-indicator.js', '85a2a830067aadad8aef0784c9253b82/highchart.js', '9048d917db8394f1812d60599c4e696d/geoheatmap.js', '9048d917db8394f1812d60599c4e696d/lollipop.js', '9048d917db8394f1812d60599c4e696d/overlapping-datalabels.js', '9048d917db8394f1812d60599c4e696d/cylinder.js', '9048d917db8394f1812d60599c4e696d/treegraph.js', '9048d917db8394f1812d60599c4e696d/gantt.js', '3705d53b4a562ee8cb985097710ab9fb/colors.js', '9048d917db8394f1812d60599c4e696d/timeline.js', 'nicegui-json-editor', '9048d917db8394f1812d60599c4e696d/histogram-bellcurve.js', '9048d917db8394f1812d60599c4e696d/sankey.js', '1dfea48bcf923d25e1a225db480f7cc3/codemirror.js', '73054f46ea4a84d3f4f6d6109b9c4918/input.js', '9048d917db8394f1812d60599c4e696d/hollowcandlestick.js', '9048d917db8394f1812d60599c4e696d/venn.js', '9048d917db8394f1812d60599c4e696d/dotplot.js', 'immutable', '24e3601540abe4bf8912bd7bf7fdc87a/xterm.js', '9048d917db8394f1812d60599c4e696d/tiledwebmap.js', '9048d917db8394f1812d60599c4e696d/annotations-advanced.js', '42be6984945cc6bca3f3ddd418462fa5/leaflet.js', '9048d917db8394f1812d60599c4e696d/draggable-points.js', '9d9cd104ae52c1a0e5cba10c513b99df/upload.js', 'c5e7e12e5fb95c76e4a1968bae2bc5ac/audio.js', 'a255b00bd038e7e31703f50806d745ce/editor.js', '9048d917db8394f1812d60599c4e696d/parallel-coordinates.js', '9048d917db8394f1812d60599c4e696d/price-indicator.js', '9048d917db8394f1812d60599c4e696d/solid-gauge.js', '9048d917db8394f1812d60599c4e696d/series-label.js', '9048d917db8394f1812d60599c4e696d/no-data-to-display.js', '9048d917db8394f1812d60599c4e696d/pareto.js', '9048d917db8394f1812d60599c4e696d/boost-canvas.js', '6f0bf4d83234b154b74255743760f273/json_editor.js', 'nicegui-mermaid', '9048d917db8394f1812d60599c4e696d/map.js', 'sass', '9048d917db8394f1812d60599c4e696d/datagrouping.js', '9048d917db8394f1812d60599c4e696d/static-scale.js', '9048d917db8394f1812d60599c4e696d/annotations.js', '9048d917db8394f1812d60599c4e696d/bullet.js', '311b3a916364748cbafd1ec125726dc6/link.js', '9048d917db8394f1812d60599c4e696d/coloraxis.js', '9048d917db8394f1812d60599c4e696d/stock.js', 'f7ea87a42c17fd372bf3d1cc9ebdaa02/highcharts-3d.js', '87f78ebeab2446c9d06e2bd3ff7e639d/teleport.js', '9048d917db8394f1812d60599c4e696d/dumbbell.js', 'e6119f8f37548b06324ee1c33a2becad/video.js', '9048d917db8394f1812d60599c4e696d/windbarb.js', 'bbe8350026bab0d6af39bb8a0e714a90/mermaid.js', '9c129ea6315e408ad8495f13c9eeece8/keyboard.js', '9048d917db8394f1812d60599c4e696d/navigator.js', '9048d917db8394f1812d60599c4e696d/drilldown.js', 'nicegui-aggrid', '9048d917db8394f1812d60599c4e696d/heikinashi.js', '563c942cd55ed96705775cebcb4ec0a1/fullscreen.js', '17f7d2c95164071d37d48e652874838c/log.js', '9048d917db8394f1812d60599c4e696d/grid-axis.js', '9048d917db8394f1812d60599c4e696d/funnel.js', '9048d917db8394f1812d60599c4e696d/vector.js', '9048d917db8394f1812d60599c4e696d/sunburst.js', '9048d917db8394f1812d60599c4e696d/funnel3d.js', 'nicegui-leaflet', 'a7f8411ba6b39ac2f5c87a6f5d69d5b2/dialog.js', '07c1ae1364d8b10fd29d1a775b4b6308/joystick.vue', '9048d917db8394f1812d60599c4e696d/data.js', '7a61f2e940db89d10a9144b18104fe27/notification.js', '9048d917db8394f1812d60599c4e696d/networkgraph.js', '9048d917db8394f1812d60599c4e696d/streamgraph.js', '9048d917db8394f1812d60599c4e696d/wordcloud.js', '9048d917db8394f1812d60599c4e696d/broken-axis.js', '9048d917db8394f1812d60599c4e696d/pattern-fill.js', '9048d917db8394f1812d60599c4e696d/organization.js', '9048d917db8394f1812d60599c4e696d/pyramid3d.js', '41faea55db14003cdbad5e718de6bc7c/plotly.vue', '9048d917db8394f1812d60599c4e696d/tilemap.js', '9048d917db8394f1812d60599c4e696d/flowmap.js', '804049bd3f5c5b5758fd3d4c8e7131b1/dark_mode.js', '9048d917db8394f1812d60599c4e696d/xrange.js', '9048d917db8394f1812d60599c4e696d/data-tools.js', '9048d917db8394f1812d60599c4e696d/pathfinder.js', 'vue', '205af4bd6c6f6faacc46894281fabf73/aggrid.js', '85df6b854b5b2c73e9edef1059725a50/scene_view.js', 'ec99af7cfc3afe482531c8e0b4416ba0/code.js', '9048d917db8394f1812d60599c4e696d/stock-tools.js', 'nicegui-codemirror', '5a50902a80aef3a24d8e55bd3f2dce20/image.js', '9048d917db8394f1812d60599c4e696d/exporting.js', '9048d917db8394f1812d60599c4e696d/mouse-wheel-zoom.js', '9048d917db8394f1812d60599c4e696d/offline-exporting.js', '2c42cff52e5ca2b2a318ba103e460885/select.js', 'nicegui-echart', '9048d917db8394f1812d60599c4e696d/heatmap.js', 'nicegui-scene', '84e0d7d61de1a56fde8aac1f7675c17a/scene.js', '9048d917db8394f1812d60599c4e696d/textpath.js', '9048d917db8394f1812d60599c4e696d/treegrid.js', '9048d917db8394f1812d60599c4e696d/item-series.js', 'nicegui-joystick', 'b2d51632d76e32bb0e7b6e3cb2a8a3d1/sub_pages.js', '9048d917db8394f1812d60599c4e696d/boost.js', 'f7ea87a42c17fd372bf3d1cc9ebdaa02/highcharts.js', '9048d917db8394f1812d60599c4e696d/variable-pie.js', '9048d917db8394f1812d60599c4e696d/arrow-symbols.js', '9048d917db8394f1812d60599c4e696d/debugger.js', '9048d917db8394f1812d60599c4e696d/treemap.js', '8aea56ea62521ea41591a1380a27d6fc/markdown.js', 'f7ea87a42c17fd372bf3d1cc9ebdaa02/highcharts-more.js', '9048d917db8394f1812d60599c4e696d/export-data.js', '9048d917db8394f1812d60599c4e696d/drag-panes.js', '9048d917db8394f1812d60599c4e696d/marker-clusters.js', '9048d917db8394f1812d60599c4e696d/pictorial.js', '9048d917db8394f1812d60599c4e696d/sonification.js', '9048d917db8394f1812d60599c4e696d/arc-diagram.js', '9048d917db8394f1812d60599c4e696d/series-on-point.js', '9048d917db8394f1812d60599c4e696d/variwide.js', '9048d917db8394f1812d60599c4e696d/accessibility.js', 'nicegui-xterm', '905954a249276abfff9937ca95d81c97/refreshable.js'}

Why are the keys showing up? We care about the names. Something isn't right...

@evnchn
Copy link
Collaborator Author

evnchn commented Dec 16, 2025

The previous commits introduced some logic issues:

3badc76 fixes how keys are found in a set which should contain only names.

This induces false-positive as InteractiveImage and InteractiveImageLayer shares component. Under existing architecture, it should allow to return the existing component if key and path match for the existing entry. Hence assert _name_is_unique(name) should be after that if-block, rectified in 426ab2c

@evnchn
Copy link
Collaborator Author

evnchn commented Dec 16, 2025

Original implementation of _name_is_unique: 0.31633496284484863 seconds

Naive implementation of _name_is_unique in old code: 0.002633333206176758 seconds

seen_names: set[str] = {'vue', 'sass', 'immutable'}

def _name_is_unique(name: str) -> bool:
    if name in seen_names:
        return False
    seen_names.add(name)
    return True

Test code:

time_start = time.time()
for _ in range(100000):
    _name_is_unique('blah')
print(f'Tested _name_is_unique 10000 times in {time.time() - time_start} seconds')

@falkoschindler Sorry but I would prefer going back. What do you think?

@evnchn
Copy link
Collaborator Author

evnchn commented Dec 16, 2025

Finally: If we need more time then maybe we ship 3.4.1 first; If not we can slide this right in.

@falkoschindler
Copy link
Contributor

Sure, the performance is better using a set of seen_names. But as far as I can tell, the time for a single call is only 3 microseconds.

The confusion between keys and names is definitely something I messed up. This is an advantage of keeping a plain set of names.

Keeping _name_is_unique domain-knowledge-free is also a valid argument. I kind of liked the fact that you can read this function to see what exactly is considered for the uniqueness requirement. But on the other hand it is also nice to keep it agnostic.

I'd love to keep the seen_names inside of _name_is_unique, like with static in C. But my only idea would be to add it as a keyword argument and intentionally violate B006 - which might heavily confuse the reader.

Bottom line: Let's leave the code as it is. Performance doesn't really matter, but architecture is simpler this way.

falkoschindler
falkoschindler previously approved these changes Dec 16, 2025
@falkoschindler falkoschindler added this pull request to the merge queue Dec 16, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Dec 16, 2025
@falkoschindler
Copy link
Contributor

Oh dear, apparently this PR breaks user code. Consider this line:

class SignaturePad(ui.element, component='signature_pad.js', esm={'signature_pad': 'dist'}):

It's certainly debatable whether the ESM module should have the same name like the Vue component. But it used to be possible, so we shouldn't break it. And if it was possible, our uniqueness constraint might be wrong.

@falkoschindler
Copy link
Contributor

Since this PR isn't urgent at all, I'll postpone it to 3.5. No need to rush.

@falkoschindler falkoschindler modified the milestones: 3.4.1, 3.5 Dec 16, 2025
@evnchn
Copy link
Collaborator Author

evnchn commented Dec 17, 2025

Yeah basically this PR should precisely capture the original behavior's boundary and throw and error, no more no less.

Right now, we have overdone it. Let's fix later.

@falkoschindler
Copy link
Contributor

[With some help from AI...]

Summary

The PR enforced a single global namespace for all names, but component names and ESM module names are in different namespaces:

  • Component names are used as JavaScript variable names: import { default as signature_pad } from "..."
  • ESM module names are used as importmap keys: imports['signature_pad'] = ...

These don't conflict, so they can share the same name.

The Fix

Separated name tracking into two namespaces:

  1. component_names — tracks component names (JS variables)
  2. import_names — tracks importmap names (ESM modules and libraries)

Created two functions:

  • _component_name_is_unique() — for component names
  • _import_name_is_unique() — for importmap names (ESM modules and libraries)

Now:

  • A component can share a name with an ESM module (different namespaces)
  • ESM modules and libraries must still be unique within the importmap namespace
  • Components must still be unique within the component namespace

This restores backward compatibility for cases like SignaturePad where the component name matches the ESM module name, while still preventing actual conflicts.

@evnchn
Copy link
Collaborator Author

evnchn commented Dec 17, 2025

So actually cross-checking was not quite right, and we are back at #5495 (comment)

My response in #5495 (comment) was a bit confused. Sorry.

@falkoschindler
Copy link
Contributor

falkoschindler commented Dec 17, 2025

No worries. I was confused as well.

In b8f54a1 I experimented with moving all assertions into the dataclasses:

  • This is as performant as using a separate set.
  • The registration functions don't have to deal with possible conflicts.
  • There is no extra state (outside of these dataclasses).
  • It is a bit more code, but better organized (I think).
  • key assertions are handled symmetrical to name assertions.

Copy link
Collaborator Author

@evnchn evnchn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spotted a logic issue

@falkoschindler falkoschindler added feature Type/scope: New or intentionally changed behavior and removed bug Type/scope: Incorrect behavior in existing functionality labels Dec 18, 2025
@falkoschindler falkoschindler added this pull request to the merge queue Dec 18, 2025
Merged via the queue into zauberzeug:main with commit dc49827 Dec 18, 2025
7 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Jan 20, 2026
… we drop it (#5500)

### Motivation

While working on
#5495 (comment),
I realize there's literally space savings on-the-table, by inspecting
code behaviour.

### Implementation

#### Observation: Only 2 places where JS dependencies are loaded

1. In the HTML, we add JS imports statically


https://github.com/zauberzeug/nicegui/blob/eea9a48071f274657e65409571c255b9b35830e8/nicegui/dependencies.py#L193-L206

2. In JS, we load them


https://github.com/zauberzeug/nicegui/blob/eea9a48071f274657e65409571c255b9b35830e8/nicegui/static/nicegui.js#L281-L290

There is no other place. I looked. 

#### Fulfill the bare-minimum for the code logic

For 1 to not trigger 2, we do the following:


https://github.com/zauberzeug/nicegui/blob/eea9a48071f274657e65409571c255b9b35830e8/nicegui/static/nicegui.js#L148-L149

But note we only care about the `.name`, nothing else. So why ship the
bulk?

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Pytests are not necessary.
  - [x] Does existing tests pass?
- [x] Documentation is not necessary (identical behaviour but faster).

### Final note

This re-affirms the belief in #5495 that we key by name, not by
hash-key.

**Is this a simple-win? I think it is if you memorize the codebase.**

---------

Co-authored-by: Falko Schindler <[email protected]>
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.

2 participants