Repository navigation
perf: let minimal pages without jQuery widgets skip loading jQuery (opt-in) - #1241
Draft
maartenbreddels wants to merge 3 commits into
Draft
maartenbreddels wants to merge 3 commits into
maartenbreddels wants to merge 3 commits into
Conversation
This was referenced Oct 5, 2026
maartenbreddels
added this pull request to stack #1242
October 5, 2026 10:42
maartenbreddels
force-pushed
the
perf/jquery-feature
branch
from
October 5, 2026 11:59
2342d5b to
86b6294
Compare
maartenbreddels
force-pushed
the
perf/jquery-feature
branch
from
October 5, 2026 12:38
86b6294 to
804611b
Compare
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>
Without the jquery feature, the stand-in $ treated every name that starts with an underscore as a probe and returned undefined. jQuery itself has four such functions ($._data, $._removeData, $._queueHooks, $._evalUrl), so a widget that calls one got "$._data is not a function" instead of the error that names +jquery, and the server logged nothing. Those four names now count as a use of jQuery. A wrapper (view.$el) keeps treating them as probes, because a real jQuery object does not have them either. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jQuery never loads on first use, so when the preloaded jquery chunk failed to run (a network error or a 404), a full page kept the stand-in for good. A widget that uses view.$el then failed with an error that told the user to add +jquery, which the page already had. The lumino and output-widget chunks recover from the same failure, because they load on first use. When jquery is on but its chunk did not run, the widget manager now loads it again, once, before it loads any model or view class, so every view gets the real jQuery. If that load fails too, the widgets keep the stand-in and the console shows why. Pages whose chunk ran do not wait for anything. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
maartenbreddels
force-pushed
the
perf/jquery-feature
branch
from
October 5, 2026 12:42
804611b to
1d3bb91
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.
jQuery moves out of the core bundle into a new
jqueryfrontend feature. This cuts about 29 KB gzip from the core on Vue 2 and Vue 3, in production builds.fullstill preloads it. Inminimal, widgets that use jQuery (anywidget widgets,solara.FigurePlotlywith plotly 6 or later, pythreejs) need+jquery. This PR is the top of a stack: it sits on the lumino PR (#1240), which sits on #1236 (the output renderers), which sits on #1235.Problem
Every Solara page downloads and runs one core bundle before it shows the first widget.
With the lumino PR (#1240), this core is 171 KB gzip on Vue 3 and 289 KB on Vue 2 (ipywidgets 8, production build).
About 29 KB of it is jQuery.
Vue widgets never call jQuery methods.
@jupyter-widgets/basewraps the element of each view with$(el), and Vue views only read the element back.Only the ipywidgets controls, the
Outputwidget, and widgets that call jQuery methods onview.$el(for example anywidget widgets and pythreejs) need jQuery.A page with only Vue widgets still pays to download and run jQuery.
An earlier CPU profile of the earlier #1236 builds gave estimates for jQuery.
It runs for about 3 ms at normal CPU speed and about 15 ms at 4x CPU slowdown.
I did not repeat that profile on these branches.
The download of 29 KB takes about 23 ms at 10 Mbit/s and about 145 ms at 1.6 Mbit/s (arithmetic, not a measurement).
Slow phones and laptops pay the most.
Change
jquery, holds jQuery.The
fullpreset (the default) preloads it, so afullpage works as in feat: load pages faster by splitting the frontend into optional features #1235.The
minimalpreset leaves it out, and+jqueryturns it on.jupyter-controlsandoutput-widgetneedjquery, because their code uses$.On ipywidgets 7 and 8 these are
Box,Controller,SelectionContainerand theOutputwidget, and on ipywidgets 7 also jQuery UI's slider.The server adds
jquerywhen you turn one of them on, andfull,-jqueryis an error while one of them is on.In the browser, their chunks load the
jquerychunk too, and run only after it ran, also inminimal.jquerychunk.A widget uses jQuery without asking for it first, so the page cannot know in time which widget needs it.
The page does not guess.
Only
full,+jquery, the controls and the Output widget load the chunk.jqueryimport of the bundle gets a small stand-in from the core (packages/solara-widget-manager/src/jquery.ts, through a webpack alias):jquerychunk runs,$(node)wraps the DOM node in an array-like object ($el[0],$el.length).That is all that
@jupyter-widgets/baseand Vue views use.This covers jQuery methods on
$el(for exampleview.$el.empty()),$("<div>"),$(function),$.ajaxand$._data:solara: this widget uses jQuery (.empty()), which this page does not load. Add +jquery to --frontend (SOLARA_FRONTEND), for example --frontend=minimal,+jquery.The server logs one warning:
A widget of the page uses the frontend feature 'jquery', which this server does not load, so the widget fails. Add "+jquery" to --frontend (SOLARA_FRONTEND) to load it, for example --frontend=minimal,+jquery.jquerychunk runs,$forwards everything to the real jQuery.This includes plugins that add to
$: jQuery UI sets$.uiand$.cleanData, and these land on the real jQuery.Backbone.$(alsowindow.Backbone.$) becomes the real jQuery, as before.fulland+jqueryrun thejquerychunk before the first view, so everyview.$elis a real jQuery object, as in feat: load pages faster by splitting the frontend into optional features #1235.jqueryis on but its preloaded chunk did not run (for example after a network error), the widget manager loads the chunk again.It does this once, before it loads any widget class.
create_viewhook, and does not guess which widget needs jQuery.It does not change the prototype of
$elobjects that exist before jQuery loads.The earlier version of perf: let minimal pages without an Output widget skip the Jupyter output renderers #1236 (commit
b17a9bcb) had all three, and Q45 dropped them.jqueryrow.The docs say that anywidget widgets (also
solara.FigurePlotlywith plotly 6 or later), pythreejs and other widgets that useview.$elneed+jqueryinminimal.jqueryfeature.Validation
I built all 8 bundles: Vue 2 and 3, ipywidgets 7 and 8, production and development.
I compared them with the build of the lumino PR (#1240) at
ed204719.I tested on three environments: Vue 3 with ipywidgets 8, Vue 2 with ipywidgets 8, and Vue 2 with ipywidgets 7.
The table shows the build after the review fixes.
Sizes are bytes after
gzip -9 -n."Full JS" is the core plus every chunk that the
fullpreset preloads.jquerygzipfullpage loads about 1.4 KB gzip more JS than with the lumino PR (perf: let minimal pages skip most of Lumino until a widget needs it #1240), in one more request.In development builds, it loads 3.6 to 3.9 KB gzip more.
jupyter-controlsandoutput-widgetchunks change by less than 10 bytes gzip.jquerychunk holds two modules: jQuery 3.7.1 and the chunk root, which hands jQuery to the stand-in.tests/unit/frontend*_test.py(three files) andtests/benchmark/summary_test.pyonce on each environment: 111 passed on each (104 and 7).After the review fixes, I rebuilt and ran the same four files again once on each environment: 111 passed on each.
The new and changed unit tests are:
test_jquery_out_of_core(new) checks all 8 builds.The core has no jQuery and has the stand-in, and the
jquerychunk has jQuery.The
lumino,jupyter-controlsandoutput-widgetchunks have no jQuery.test_parse_closure(changed) now also checks that+jupyter-controlsand+output-widgetbringjquery, and thatfull,-jqueryis an error.test_missing_message_logs_once(new) checks that the server logs one warning that names+jquery, andtest_missing_message_not_in_full(new) checks that afullserver logs nothing.tests/integration/frontend_chunks_test.pyonce on each environment, with the flask and the starlette server: 28 passed on each.After the review fixes, I ran the same 14 tests plus the new
test_full_jquery_request_failsonce on each environment: 30 passed on each.The new and changed tests are:
test_jquery[full]andtest_jquery[minimal,+jquery]: a page widget whose view is not Vue based callsview.$el.empty().append(...),.find()and.css(), and works.The
$elof every DOM view of the page is a real jQuery object, for the Vue view and the other view.window.Backbone.$is jQuery.The page requests the
jquerychunk once, and shows no warning and no error.test_jquery[minimal]: the same widget fails, and the page requests nojquerychunk.The console shows the error that names
+jqueryonce.On ipywidgets 8, the error view of the widget shows the same text.
The server logs one warning that names
+jquery.A call to
Backbone.$._data(...)throws the same kind of error, for$._data.test_full_jquery_request_fails[flask|starlette]: infull, the first request for thejquerychunk fails with a 404.The page requests the chunk a second time, the jQuery widget works, and every
$elis a real jQuery object.The server logs no warning.
Before the fix, I reproduced the bug in the browser: after the 404, the widget showed the error that tells you to add
+jquery. I kept no log of that run.test_minimal_lazy_controls: inminimal, anIntSliderloadsjupyter-controlswith one warning that namesjupyter-controls.The
jquerychunk loads once with it, without a warning.The right arrow key moves the slider from 3 to 4. On ipywidgets 7, this slider is jQuery UI's.
test_tab[full]andtest_tab[minimal]also check that the page requestsjqueryonce.test_lumino_requirejshas two page widgets. The first one asks requirejs only for modules that the core defines.It no longer uses jQuery, so in
minimalit still works withoutjquery, and the page loads noluminochunk for it.test_full_preloads_sync,test_full_lumino_request_fails,test_full_display_outputs,test_minimal_lazy_output,test_full_output_widget_request_failsandtest_minimal_soft_remount_keeps_one_widget_renderer.Gaps
The 3 ms and 15 ms come from an earlier CPU profile of the earlier perf: let minimal pages without an Output widget skip the Jupyter output renderers #1236 builds: 3.2 ms at 1x, and 15.5 to 16 ms at 4x, on Vue 2 and Vue 3.
That profile ran under Playwright on 10 to 20 cold loads from localhost, so these numbers are estimates of what real users pay.
jQuery throws and catches one error when it starts, and while DevTools or Playwright is attached, V8 then parses the functions on the stack again.
In the earlier profile, that cost 46 ms at 1x and 211 ms at 4x on the Vue 3 build of feat: load pages faster by splitting the frontend into optional features #1235.
I conclude from V8's source that real users do not pay this cost, but I could not measure it without DevTools.
So a Playwright benchmark such as
tests/benchmarkwill likely show a larger win inminimalthan real users get.This is an estimate: I did not run the benchmark.
There, a widget that uses jQuery in
minimaldoes not show, and only the browser console shows the error.minimal, a view that exists before the controls or theOutputwidget load jQuery keeps its stand-in$el.A widget that uses jQuery and renders before the first control fails, and its error view stays.
Widgets that render after the first control work.
A later jQuery call on the old view throws an error that says the page loaded jQuery only after the view was made.
$is not the same object as jQuery; onlyBackbone.$is.Code that checks
$ === jQuerysees a difference.I did not look for such code in other widget libraries.
jquerychunk also fails, the widgets keep the stand-in.Their error then says to add
+jquery, which the page already has.While that second load runs, no widget class loads.
A request that hangs delays every widget, up to the 120 s chunk timeout of webpack.
jquery.jQuery runs there only after a control or the Output widget loads it, so widgets that use jQuery before that fail.
Their error says to add
+jquery, but that cannot help, because such a template preloads no features.Before this PR, jQuery was in the core, so these widgets worked there.
solara.FigurePlotlyor pythreejs widgets.The browser test uses a page widget that calls the same
$elmethods.The anywidget notebook extension calls
view.$el.empty().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, so here they also needjquery. As a driver default, thecreate_viewhook, the prototype swap and the guessing go. For jQuery, this deviates from Q37 (a widget always works) for widgets inminimalthat are not Vue based, by Maarten's call. It also reverses the Q39 outcome that jQuery stays in the core.+jquery, and no lazy load. Maarten accepted that anywidget,solara.FigurePlotlyand pythreejs need+jqueryinminimal. Lumino loads lazily with one warning that names the module that asked for it, in the lumino PR (perf: let minimal pages skip most of Lumino until a widget needs it #1240). 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
Two rounds, with two reviewers: astra and gpt-6.1-sol.
Both come from one vendor, OpenAI. Maarten chose them in Q41.
Round 1: both reviewed the one commit of the first version (
5995f5de) from the commit alone, without builds or tests.astra found one MEDIUM finding, and gpt-6.1-sol found one MEDIUM and one LOW finding.
Both defects were real.
I reproduced each of them in the browser before I fixed it, but kept no logs of these runs.
jquerychunk before views are made.When the preloaded
jquerychunk failed once infullorminimal,+jquery, the page kept the stand-in for good.A widget that uses
view.$elthen failed with an error that told the user to add+jquery, which the page already had.2342d5b1. The widget manager loads the chunk again, once, before it loads any model or view class. The new testtest_full_jquery_request_failscovers it.luminoandoutput-widget. Those features load on first use, so they recover on their own.jqueryis the first feature that never loads on first use, and the jquery commit did not add the same failure test for it.+jqueryerror.Without
jquery, the stand-in let every name that starts with_pass as a probe.A call such as
Backbone.$._data(node, "events")then failed withis not a function, without the+jqueryerror, and the server logged nothing.fdd9afd3. The four functions of jQuery 3.7 that start with_(_data,_removeData,_queueHooks,_evalUrl) now count as a use of jQuery. Thetest_jquery[minimal]test checks the error for$._data.minimal.Test each name that both match.
Fix sizes:
fdd9afd3changes 8 lines (4 injquery.ts, 4 in the test).2342d5b1changes 66 lines (22 injquery.ts, 6 inmanager.ts, 38 in the test), and adds the exported functionenabledJQueryLoaded.Round 2: astra and gpt-6.1-sol reviewed
2342d5b1(the retry of thejquerychunk, 66 lines) again, and found nothing.They reviewed that commit alone, not the whole diff of this PR.
astra, which raised the finding, confirmed
fdd9afd3(8 lines:$._dataand the other three functions now name+jquery) as a small fix, with no new defect.astra and an Opus agent also checked the PR descriptions against the code, and this version of the description has their corrections.
🤖 Generated with Claude Code