Skip to content

perf: let minimal pages without an Output widget skip the Jupyter output renderers - #1236

Draft
maartenbreddels wants to merge 3 commits into
t3code/speed-page-and-kernel-loadfrom
perf/classic-widgets-core
Draft

maartenbreddels wants to merge 3 commits into
t3code/speed-page-and-kernel-loadfrom
perf/classic-widgets-core

Conversation

@maartenbreddels

@maartenbreddels maartenbreddels commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

The Jupyter output renderers leave the core bundle, so pages that leave out output-widget, like a minimal page without an Output widget, skip them. In production builds, the core shrinks by about 33 KB gzip on Vue 3 and 29 KB on Vue 2. The Vue 3 number includes two Vue build flags. This PR is now the bottom of a stack on #1235. jQuery and Lumino stay in the core here, and move out in the lumino PR (#1240) and the jquery PR (#1241).

Problem

Every Solara page downloads and runs one core bundle before it shows the first widget.
After #1235, this core is still 244 KB gzip on Vue 3 and 359 KB on Vue 2 (ipywidgets 8, production build).
About 29 KB of it is the Jupyter output renderers: the code that shows HTML, Markdown, LaTeX, images and JavaScript outputs.
Only the ipywidgets Output widget (for example solara.display of an IPython object) uses them.
Most pages have no Output widget, but every page pays to download and run this code.
Slow phones and laptops pay the most.

The Vue 3 core also keeps code for two Vue build flags that Solara never changes, because the build does not tell Vue their values.

Change

  • The --frontend setting of feat: load pages faster by splitting the frontend into optional features #1235 picks the frontend features that a page preloads.
    The preset full (the default) preloads all of them.
    The preset minimal leaves most of them out, and the browser loads those on first use, with a warning.
  • The output renderers move into the output-widget chunk.
    These are @jupyterlab/rendermime with marked, the HTML sanitizer of @jupyterlab/apputils, @jupyterlab/translation, and the JavaScript renderer.
  • The core keeps a small RenderMimeRegistry stand-in.
    It remembers its options and the renderers added to it.
    When the output-widget chunk runs, the stand-in makes a real registry with the same options and renderers, and passes every call on to it.
  • full preloads output-widget.
    The chunk then runs before the page makes its registry, so a full page gets a real registry, as in feat: load pages faster by splitting the frontend into optional features #1235.
  • In minimal, the first Output widget loads output-widget, as in feat: load pages faster by splitting the frontend into optional features #1235.
    Before that, the stand-in accepts only addFactory, clone and its option fields.
    Any other use, for example createRenderer, getFactory or a read of mimeTypes, throws an error and starts the load.
    The docs now say this for a rendering call such as createRenderer.
  • Like the real registry, the stand-in drops a renderer that a later renderer fully replaces.
    A soft remount happens when a page reconnects to a new kernel: the page makes a new widget manager and renders the app again, without a reload.
    Each soft remount adds a renderer that holds the new widget manager.
    Without this fix, a page that never loads the Output widget keeps every old widget manager in memory.
  • The Vue 3 build defines __VUE_OPTIONS_API__ (on) and __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ (off), their runtime defaults.
    The minifier then removes the runtime checks for both flags, and the code that the hydration mismatch details flag turns off.
    The Options API stays on.
    __VUE_PROD_DEVTOOLS__ stays a runtime global, so a page can still turn on the Vue devtools in production.
  • This version of the PR is smaller than the earlier version (commit b17a9bcb):

Validation

I built all 8 bundles once: Vue 2 and 3, ipywidgets 7 and 8, production and development.
I compared them with the build of #1235 at 60586ce8.
Sizes are bytes after gzip -9 -n.
"Full JS" is the core plus every chunk that the full preset preloads.

build core gzip, #1235 core gzip, this PR output-widget gzip full JS gzip
Vue 3, ipywidgets 8, prod 244,341 211,539 9,442 to 39,431 610,763 to 607,910
Vue 3, ipywidgets 7, prod 239,140 206,203 9,445 to 39,626 596,589 to 594,021
Vue 2, ipywidgets 8, prod 358,887 329,787 9,440 to 39,429 529,563 to 530,452
Vue 2, ipywidgets 7, prod 353,632 324,386 9,444 to 39,625 530,522 to 531,686
Vue 3, ipywidgets 8, dev 643,542 586,152 27,336 to 86,674 1,359,633 to 1,361,553
Vue 2, ipywidgets 8, dev 835,829 778,631 27,335 to 86,672 1,189,575 to 1,191,714
  • In production builds, the renderers save about 29 KB gzip on both Vue versions.
    The two Vue 3 flags save about 3.7 KB more on Vue 3 (the Vue 3 saving minus the Vue 2 saving).
  • A full page loads the same chunks as in feat: load pages faster by splitting the frontend into optional features #1235.
    In production builds, it loads about the same bytes: 3 KB less on Vue 3, 1 KB more on Vue 2.
    In development builds, it loads 1.9 to 3.1 KB more on both Vue versions.
  • The dist folders have the same files as the feat: load pages faster by splitting the frontend into optional features #1235 build.
  • I ran tests/unit/frontend*_test.py once on each of three environments (Vue 3 with ipywidgets 8, Vue 2 with ipywidgets 8, Vue 2 with ipywidgets 7): 84 passed on each.
    Two of these tests are new:
    • test_output_renderers_out_of_core checks for all 8 builds that a class name of the Markdown renderer is in the output-widget chunk and not in the core.
    • test_vue3_feature_flags_defined checks that the Vue 3 cores no longer name the two flags, and still name __VUE_PROD_DEVTOOLS__.
  • I ran 6 browser tests of tests/integration/frontend_chunks_test.py once on each of the same three environments, with the flask and the starlette server: 12 passed on each.
    The tests are test_full_preloads_sync, test_full_display_outputs, test_minimal_lazy_controls, and three new ones:
    • test_minimal_lazy_output: a minimal page without an Output widget does not load output-widget; a page with one loads it once, and its Markdown outputs render.
    • test_full_output_widget_request_fails: the preloaded output-widget chunk fails once with a 404; the page still starts, loads the chunk on first use, and renders the outputs.
    • test_minimal_soft_remount_keeps_one_widget_renderer: after a soft remount, the stand-in holds one widget renderer, not two.
      I removed the fix once and rebuilt: this test then failed with 2 renderers.
      After I put the fix back, the rebuilt Vue 3 dist was byte for byte the same as before.

Gaps

  • I did not measure load or run times on this branch.
    An earlier CPU profile of the earlier perf: let minimal pages without an Output widget skip the Jupyter output renderers #1236 builds gave estimates for the renderer code.
    It runs for about 2 ms at normal CPU speed and about 10 ms at 4x slowdown.
    The same profile found no clear time change from the two Vue 3 flags.
    I did not repeat that profile on this branch.
  • I did not test third-party widgets that call the page's RenderMimeRegistry themselves.
    In minimal, such a widget gets an error for most calls until output-widget loads.
  • The docs name only rendering calls, such as createRenderer, as calls that throw before output-widget loads.
    Other calls, such as getFactory or a read of mimeTypes, throw too.
  • On Vue 3, Vue no longer sets the globals window.__VUE_OPTIONS_API__ and window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ at startup, because the build now fixes their values.
    I know of no code that reads them.

Align results

The #1235 description lists the earlier questions of this work, Q0 to Q46.
These three settle this PR:

Crossreview results

Rebase onto #1235 at e8766df (2026-10-23): one new test: commit expects the vue-sfc warning that ipyvue 3.1 gives on minimal Vue 3 pages. astra and gpt-6.1-sol compared the old and new commits with git range-diff and found no regressions.

Round 1 had two reviewers: astra and gpt-6.1-sol.
Both come from one vendor, OpenAI. Maarten chose them in Q41.
Both reviewed the two commits of this PR (6a7caa63 and 6e151ed0) from the commits alone, without builds or tests.
Neither reviewer found a defect, so there are no CRITICAL, HIGH or MEDIUM findings, no fixes, and no small fixes.
Constitution change: None.

Round 2: astra and an Opus agent checked the PR descriptions against the code, and this version of the description has their corrections.

🤖 Generated with Claude Code

@maartenbreddels
maartenbreddels force-pushed the perf/classic-widgets-core branch from f0a118b to b17a9bc Compare October 5, 2026 07:10
@maartenbreddels
maartenbreddels force-pushed the perf/classic-widgets-core branch from b17a9bc to 6e151ed Compare October 5, 2026 10:29
@maartenbreddels maartenbreddels changed the title perf: start pages faster by loading jQuery, most of Lumino and the output renderers only when needed perf: let minimal pages without an Output widget skip the Jupyter output renderers Oct 5, 2026
maartenbreddels added a commit that referenced this pull request Oct 5, 2026
Every page downloaded and ran jQuery (about 29 KB gzip) before its first
widget, but Vue widgets never use it: @jupyter-widgets/base only wraps the
element of each view with $(el). Only the ipywidgets controls, the Output
widget and widgets that use view.$el (anywidget, pythreejs, ...) need it.

jQuery is now the frontend feature jquery. The full preset preloads it, so
full pages work as before; minimal leaves it out, and +jquery turns it on.
jupyter-controls and output-widget need it, and their chunks run after it
(jQuery UI's slider in the ipywidgets 7 controls uses $ when its module
runs). It never loads on first use, because a widget uses jQuery without
asking for it first, and guessing which widget needs it is what #1236 did
and we dropped.

Every 'jquery' import of the bundle gets a small stand-in from the core.
Until the jquery chunk runs, $(node) wraps DOM nodes, which is all that
Vue views use, and any other use of jQuery throws an error that names
+jquery. The browser console shows it, ipywidgets 8 shows it in the error
view of the widget, and the server logs it once. Once the chunk runs, the
stand-in forwards to jQuery and Backbone.$ is jQuery, so in full every
view.$el is a real jQuery object, as before.

The core shrinks by about 29 KB gzip on Vue 2 and Vue 3. A full page loads
about 1.3 KB gzip more, in one more request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels
maartenbreddels force-pushed the perf/classic-widgets-core branch from 6e151ed to 0d5c515 Compare October 5, 2026 11:59
maartenbreddels added a commit that referenced this pull request Oct 5, 2026
Every page downloaded and ran jQuery (about 29 KB gzip) before its first
widget, but Vue widgets never use it: @jupyter-widgets/base only wraps the
element of each view with $(el). Only the ipywidgets controls, the Output
widget and widgets that use view.$el (anywidget, pythreejs, ...) need it.

jQuery is now the frontend feature jquery. The full preset preloads it, so
full pages work as before; minimal leaves it out, and +jquery turns it on.
jupyter-controls and output-widget need it, and their chunks run after it
(jQuery UI's slider in the ipywidgets 7 controls uses $ when its module
runs). It never loads on first use, because a widget uses jQuery without
asking for it first, and guessing which widget needs it is what #1236 did
and we dropped.

Every 'jquery' import of the bundle gets a small stand-in from the core.
Until the jquery chunk runs, $(node) wraps DOM nodes, which is all that
Vue views use, and any other use of jQuery throws an error that names
+jquery. The browser console shows it, ipywidgets 8 shows it in the error
view of the widget, and the server logs it once. Once the chunk runs, the
stand-in forwards to jQuery and Backbone.$ is jQuery, so in full every
view.$el is a real jQuery object, as before.

The core shrinks by about 29 KB gzip on Vue 2 and Vue 3. A full page loads
about 1.3 KB gzip more, in one more request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels
maartenbreddels force-pushed the perf/classic-widgets-core branch from 0d5c515 to c6ef3f9 Compare October 5, 2026 12:38
maartenbreddels added a commit that referenced this pull request Oct 5, 2026
Every page downloaded and ran jQuery (about 29 KB gzip) before its first
widget, but Vue widgets never use it: @jupyter-widgets/base only wraps the
element of each view with $(el). Only the ipywidgets controls, the Output
widget and widgets that use view.$el (anywidget, pythreejs, ...) need it.

jQuery is now the frontend feature jquery. The full preset preloads it, so
full pages work as before; minimal leaves it out, and +jquery turns it on.
jupyter-controls and output-widget need it, and their chunks run after it
(jQuery UI's slider in the ipywidgets 7 controls uses $ when its module
runs). It never loads on first use, because a widget uses jQuery without
asking for it first, and guessing which widget needs it is what #1236 did
and we dropped.

Every 'jquery' import of the bundle gets a small stand-in from the core.
Until the jquery chunk runs, $(node) wraps DOM nodes, which is all that
Vue views use, and any other use of jQuery throws an error that names
+jquery. The browser console shows it, ipywidgets 8 shows it in the error
view of the widget, and the server logs it once. Once the chunk runs, the
stand-in forwards to jQuery and Backbone.$ is jQuery, so in full every
view.$el is a real jQuery object, as before.

The core shrinks by about 29 KB gzip on Vue 2 and Vue 3. A full page loads
about 1.3 KB gzip more, in one more request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels
maartenbreddels force-pushed the perf/classic-widgets-core branch from c6ef3f9 to 5d98bf1 Compare October 5, 2026 12:42
maartenbreddels and others added 3 commits October 23, 2026 19:32
… for the Output widget

Every page ran the Jupyter output renderers (@jupyterlab/rendermime with
marked, the apputils sanitizer and translation, and the javascript
renderer) at startup. Only the Output widget renders outputs, so pages
without one paid about 29 KB gzip of the core bundle for nothing, on both
Vue versions.

The renderers move into the output-widget chunk. The core keeps a small
RenderMimeRegistry stand-in that remembers its options and the factories
added to it, and becomes the real registry when the chunk runs. The full
preset preloads the chunk, which then runs before the page makes its
registry, so such a page gets a real registry, as before.

Like the real registry, the stand-in drops a factory that a later one
fully replaces. Each soft remount adds a factory that holds the new
widget manager; without this, a page that never loads the Output widget
would keep every old manager in memory.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Vue's esm-bundler build checks its feature flags at runtime when the
bundler does not define them, so the core kept the code for flags that
are never set. Defining __VUE_OPTIONS_API__ (on) and
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ (off), their runtime defaults,
lets terser drop that code: about 4 KB gzip of the Vue 3 core.
__VUE_PROD_DEVTOOLS__ stays a runtime global, so a page can still turn on
the Vue devtools in production, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With ipyvue 3.1.0, every Vue 3 page compiles its Vue template with the
lazy vue-sfc chunk, and the minimal preset leaves that chunk out
(decision Q43). The test of the lazy output renderers opens two minimal
pages, so the browser and the server now also warn for vue-sfc. The test
did not expect that warning and failed on Vue 3 after the rebase onto
the new frontend features branch.

The test now adds the vue-sfc warnings from the _vue_sfc_warnings
helper to the expected lazy features, as the other minimal tests do.
With ipyvue 3.0 or Vue 2 the helper gives no warnings, so nothing
changes there.

Validation: test_minimal_lazy_output passes with flask and starlette on
Vue 3 (ipyvue 3.1.0, ipyvuetify 3.1.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maartenbreddels added a commit that referenced this pull request Oct 23, 2026
Every page downloaded and ran jQuery (about 29 KB gzip) before its first
widget, but Vue widgets never use it: @jupyter-widgets/base only wraps the
element of each view with $(el). Only the ipywidgets controls, the Output
widget and widgets that use view.$el (anywidget, pythreejs, ...) need it.

jQuery is now the frontend feature jquery. The full preset preloads it, so
full pages work as before; minimal leaves it out, and +jquery turns it on.
jupyter-controls and output-widget need it, and their chunks run after it
(jQuery UI's slider in the ipywidgets 7 controls uses $ when its module
runs). It never loads on first use, because a widget uses jQuery without
asking for it first, and guessing which widget needs it is what #1236 did
and we dropped.

Every 'jquery' import of the bundle gets a small stand-in from the core.
Until the jquery chunk runs, $(node) wraps DOM nodes, which is all that
Vue views use, and any other use of jQuery throws an error that names
+jquery. The browser console shows it, ipywidgets 8 shows it in the error
view of the widget, and the server logs it once. Once the chunk runs, the
stand-in forwards to jQuery and Backbone.$ is jQuery, so in full every
view.$el is a real jQuery object, as before.

The core shrinks by about 29 KB gzip on Vue 2 and Vue 3. A full page loads
about 1.3 KB gzip more, in one more request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels
maartenbreddels force-pushed the perf/classic-widgets-core branch from 5d98bf1 to 1f51b86 Compare October 23, 2026 18:22

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant