Repository navigation
perf: let minimal pages without an Output widget skip the Jupyter output renderers - #1236
Draft
maartenbreddels wants to merge 3 commits into
Draft
maartenbreddels wants to merge 3 commits into
maartenbreddels wants to merge 3 commits into
Conversation
maartenbreddels
force-pushed
the
perf/classic-widgets-core
branch
from
October 5, 2026 07:10
f0a118b to
b17a9bc
Compare
This was referenced Oct 5, 2026
maartenbreddels
force-pushed
the
perf/classic-widgets-core
branch
from
October 5, 2026 10:29
b17a9bc to
6e151ed
Compare
This was referenced 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
force-pushed
the
perf/classic-widgets-core
branch
from
October 5, 2026 11:59
6e151ed to
0d5c515
Compare
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
force-pushed
the
perf/classic-widgets-core
branch
from
October 5, 2026 12:38
0d5c515 to
c6ef3f9
Compare
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
force-pushed
the
perf/classic-widgets-core
branch
from
October 5, 2026 12:42
c6ef3f9 to
5d98bf1
Compare
… 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
force-pushed
the
perf/classic-widgets-core
branch
from
October 23, 2026 18:22
5d98bf1 to
1f51b86
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Jupyter output renderers leave the core bundle, so pages that leave out
output-widget, like aminimalpage 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
Outputwidget (for examplesolara.displayof 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
--frontendsetting 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
minimalleaves most of them out, and the browser loads those on first use, with a warning.output-widgetchunk.These are
@jupyterlab/rendermimewithmarked, the HTML sanitizer of@jupyterlab/apputils,@jupyterlab/translation, and the JavaScript renderer.RenderMimeRegistrystand-in.It remembers its options and the renderers added to it.
When the
output-widgetchunk runs, the stand-in makes a real registry with the same options and renderers, and passes every call on to it.fullpreloadsoutput-widget.The chunk then runs before the page makes its registry, so a
fullpage gets a real registry, as in feat: load pages faster by splitting the frontend into optional features #1235.minimal, the first Output widget loadsoutput-widget, as in feat: load pages faster by splitting the frontend into optional features #1235.Before that, the stand-in accepts only
addFactory,cloneand its option fields.Any other use, for example
createRenderer,getFactoryor a read ofmimeTypes, throws an error and starts the load.The docs now say this for a rendering call such as
createRenderer.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.
__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.b17a9bcb):classic-widgetsfeature is gone.semverport and its license commit are dropped.@jupyterlab/coreutils, the kernelspec API andminimistare dropped, and so is their build check,StandInGuardPlugin.create_viewhook, the prototype swap of$elobjects and the list of guessed modules are dropped.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
fullpreset preloads.output-widgetgzipThe two Vue 3 flags save about 3.7 KB more on Vue 3 (the Vue 3 saving minus the Vue 2 saving).
fullpage 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.
tests/unit/frontend*_test.pyonce 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_corechecks for all 8 builds that a class name of the Markdown renderer is in theoutput-widgetchunk and not in the core.test_vue3_feature_flags_definedchecks that the Vue 3 cores no longer name the two flags, and still name__VUE_PROD_DEVTOOLS__.tests/integration/frontend_chunks_test.pyonce 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: aminimalpage without an Output widget does not loadoutput-widget; a page with one loads it once, and its Markdown outputs render.test_full_output_widget_request_fails: the preloadedoutput-widgetchunk 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
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.
RenderMimeRegistrythemselves.In
minimal, such a widget gets an error for most calls untiloutput-widgetloads.createRenderer, as calls that throw beforeoutput-widgetloads.Other calls, such as
getFactoryor a read ofmimeTypes, throw too.window.__VUE_OPTIONS_API__andwindow.__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:
full(as in feat: load pages faster by splitting the frontend into optional features #1235), off inminimal, opt in with+jqueryand+lumino.jupyter-controlsandoutput-widgetbring what their code imports. As a driver default, thecreate_viewhook, the prototype swap and the guessing go. This PR keeps jQuery and Lumino in the core, as in feat: load pages faster by splitting the frontend into optional features #1235.+jquery, and no lazy load. Lumino loads lazily with one warning that names the module that asked for it. Both changes are in their own PRs. Thesemverport is dropped. As a driver default, the stand-ins for@jupyterlab/coreutils, the kernelspec API andminimistare dropped too. The work file gives "under 2 KB, under 1 ms" as the reason. An earlier measurement put theminimistand kernelspec stand-ins at about 1.6 KB gzip and under 0.5 ms at normal CPU speed. Without the stand-ins, all of@jupyterlab/coreutilsand@lumino/pollingstay in the core. Nobody measured that total on these branches.Crossreview results
Rebase onto #1235 at e8766df (2026-10-23): one new
test:commit expects thevue-sfcwarning that ipyvue 3.1 gives on minimal Vue 3 pages. astra and gpt-6.1-sol compared the old and new commits withgit range-diffand 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 (
6a7caa63and6e151ed0) 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