Skip to content

anywidget front-end throws on the second widget instance in a page #3286

Closed

Description

@cpsievert

Summary

anywidget evaluates a widget's _esm module once per widget model (each model gets a fresh Blob URL import). Perspective's perspective-anywidget.js bundle performs page-global side effects at module scope that throw on the second evaluation, so any page with two Perspective widget models — or one re-created model — renders nothing. Two independent failure layers:

  1. Unguarded customElements.define() for regular-layout, regular-layout-frame, and regular-layout-tab (bundled regular-layout dependency). Second evaluation throws NotSupportedError: ... the name "regular-layout" has already been used with this registry. (Long-standing; present in 5.6.1.)
  2. New in 5.7.0 (via Better WASM init #3269): module-scope perspective.init({ viewer, server }) registers into a globalThis-shared WASM registry that throws "viewer wasm is already initialized" on the second registration.

Why this breaks

anywidget's runtime imports _esm through a fresh Blob URL per model (load.ts), so the browser's module cache never dedupes: module-scope code runs once per widget model, while customElements and globalThis are page-global. #3192 already applied skip-and-warn guards to Perspective's own custom elements for exactly this reason; the bundled regular-layout registration and the newer WASM registry are the two spots that work didn't cover.

Minimal reproduction (pure JavaScript, no Python)

Serve the shipped perspective/widget/static/perspective-anywidget.js (here from perspective-python==5.9.0) and import it the way anywidget's runtime does:

<script type="module">
  const src = await (await fetch("/perspective-anywidget.js")).text();

  async function importWidgetModule(label) {
    const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
    try {
      await import(url);
      console.log(`${label}: ok`);
    } catch (e) {
      console.error(`${label}: ${e.message}`);
    } finally {
      URL.revokeObjectURL(url);
    }
  }

  await importWidgetModule("widget-1"); // ok
  await importWidgetModule("widget-2"); // NotSupportedError: "regular-layout" already defined
</script>

With the define collision patched over, the second evaluation then fails on 5.7.0+ with Error: viewer wasm is already initialized (as an unhandled rejection, leaving the exported ready promise permanently rejected).

Real-world impact

This affects any host that renders anywidget models through the generic runtime — JupyterLab's widget manager path, VS Code notebooks, marimo, Panel, etc. Verified with perspective-python 5.6.1 and 5.9.0 in a current JupyterLab:

import pandas as pd
from perspective.widget import PerspectiveWidget

df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
PerspectiveWidget(df)   # cell 1 — fine on a fresh page
PerspectiveWidget(df)   # cell 2 — "Error displaying widget"

The same failure hits a single widget whose model is re-created — re-running the cell, or reopening a notebook whose saved widget output re-renders next to a freshly executed one. So "show two tables side by side" and "re-run this cell" are both broken on the anywidget path. (Related: with today's pip install jupyterlab perspective-python, the perspective labextension can't activate — it requires @jupyter-widgets/base ^2 || ^3 || ^4 while JupyterLab provides 6.x — so notebook users plausibly are on that path right now. Happy to file that separately.)

Fix

A PR implementing both fixes is open at #3287 — small patch (3 files, ~150 lines including tests), validated end-to-end: with it, both module evaluations above succeed and resolve to the same runtime, and single-evaluation behavior is unchanged.

Environment

  • perspective-python 5.6.1 (define issue), 5.7.0–5.9.0 (define issue + WASM registry issue)
  • anywidget 0.11.0, Chromium via Playwright; reproduced in a bare HTML page and in JupyterLab

Activity

  1. changed the title [-]anywidget front-end throws on the second widget instance in a page: module-scope `customElements.define` and (since 5.7.0) WASM registry `init()` are not re-evaluation safe[/-] [+]anywidget front-end throws on the second widget instance in a page[/+] on Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions