Repository navigation
perf: let minimal pages skip most of Lumino until a widget needs it - #1240
Draft
maartenbreddels wants to merge 2 commits into
Draft
maartenbreddels wants to merge 2 commits into
maartenbreddels wants to merge 2 commits into
Conversation
This was referenced Oct 5, 2026
maartenbreddels
force-pushed
the
perf/lumino-feature
branch
from
October 5, 2026 11:59
ed20471 to
0fc640c
Compare
maartenbreddels
added a commit
that referenced
this pull request
Oct 5, 2026
The frozen app had no copy of the bundle that `npm run devlink` links into the CDN cache, so it fetched the published bundle from the CDN. That bundle lacks the chunk files this PR's page asks for, so every page load made extra CDN requests for missing files. When one of them was slow, the page did not finish loading within the test's 30 s, and the job failed now and then (twice on #1240, once here). Pointing SOLARA_ASSETS_PROXY_CACHE_DIR at the linked cache makes the test use this PR's bundle, like the other jobs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
maartenbreddels
force-pushed
the
perf/lumino-feature
branch
2 times, most recently
from
October 5, 2026 12:42
c8cc890 to
b1ee79a
Compare
…hat use it Every page ran all of Lumino at startup, but the core only needs Widget, Panel, MessageLoop, signaling and domutils. The rest (DockPanel, TabBar, menus, commands, virtualdom, ...) is for the ipywidgets controls, the Output widget, and widgets of notebook extensions that ask requirejs for @lumino/* or @phosphor/*. Moving it out shrinks the core by about 40 KB gzip, on Vue 2 and Vue 3. The new frontend feature lumino holds that code. The full preset preloads it and minimal leaves it out. jupyter-controls and output-widget need it: webpack loads their chunks from the lumino chunk, so they run after it and share its modules. A requirejs request for a moved module loads lumino on first use, with one warning in the browser and one in the server log that name the module that asked, because only the browser knows which widget needs it. @lumino/signaling, @lumino/domutils (and their @phosphor names) and @jupyterlab/coreutils stay in the core, so widgets that only ask for those load nothing. Webpack cannot tree-shake Lumino's flat dist files, so tools/split-lumino.js compiles the sources of @lumino/widgets, algorithm and collections to one module per file, with TypeScript 3.6 (the version those packages were built with, so classes stay ES5 and accessors stay enumerable). The build fails when a package version is not the pinned one, when the accessors differ from dist, or when the split is stale. A preloaded chunk whose required chunk did not load (for example a 404 for lumino) no longer runs half-way: it counts as missing, and loads with what it needs on first use. 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 minimal runs of test_lumino_requirejs and test_tab did not expect the vue-sfc warning and failed on Vue 3 after the rebase onto the new frontend features branch. The tests now expect one browser warning per page and one server warning for vue-sfc from the _vue_sfc_warnings helper. They find the lumino and jupyter-controls server warnings by their text, not by their position, because the vue-sfc warning can come first. With ipyvue 3.0 or Vue 2 the helper gives no warnings, so nothing changes there. Validation: test_lumino_requirejs, test_tab and test_full_lumino_request_fails pass 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
force-pushed
the
perf/lumino-feature
branch
from
October 23, 2026 18:22
b1ee79a to
92518a6
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.
Most of Lumino moves out of the core bundle into a new
luminofrontend feature. This cuts about 40 KB gzip from the core on Vue 2 and Vue 3, in production builds.fullstill preloads it, so onlyminimalpages skip it, until a widget needs it. This PR is part of a stack: it sits on #1236 (the output renderers), which sits on #1235. jQuery stays in the core here, and moves out in the jquery PR (#1241).Problem
Every Solara page downloads and runs one core bundle before it shows the first widget.
With #1236, this core is still 212 KB gzip on Vue 3 and 330 KB on Vue 2 (ipywidgets 8, production build).
About 40 KB of it is Lumino code that most pages never run: dock panels, tab bars, menus, commands and a virtual DOM.
Only the ipywidgets controls (for example
Tab), theOutputwidget, and widgets of notebook extensions that ask requirejs for@lumino/*or@phosphor/*use it.A page with only Vue widgets pays to download and run this code anyway.
Slow phones and laptops pay the most.
Change
lumino, holds the Lumino code that the core does not use.The core keeps what it needs:
Widget,Panel,MessageLoop,@lumino/signaling,@lumino/domutilsand a few helpers.fullpreset (the default) preloadslumino, so afullpage works as in feat: load pages faster by splitting the frontend into optional features #1235.The
minimalpreset leaves it out.+luminoturns it on.jupyter-controlsandoutput-widgetneedlumino.The server adds it when you turn one of them on, and
full,-luminois an error while one of them is on, as forjupyter-css.In the bundle, their chunks load from the
luminochunk, so they run after it and share its code.@lumino/widgets,@lumino/algorithm,@lumino/commandsand@lumino/virtualdom(and their@phosphor/*names) come from theluminochunk.When a page without
luminogets the first request for one of them, the browser loads the chunk once.It then logs one warning that names the feature and the module that asked, and the server logs one line with the same names:
solara: frontend feature "lumino" was not preloaded, it loads now for the requirejs module "@lumino/widgets". Add "+lumino" to --frontend (SOLARA_FRONTEND) to preload it.The requirejs module '@lumino/widgets' of the page needs the frontend feature 'lumino', which this server does not preload. ...luminoalong, the warning namesjupyter-controlsoroutput-widget, as before, and notlumino.@lumino/signaling,@lumino/domutils(and their@phosphor/*names) and@jupyterlab/coreutils.A widget that asks only for those loads nothing more.
The new script
packages/solara-widget-manager/tools/split-lumino.jscompiles the sources of@lumino/widgets,@lumino/algorithmand@lumino/collectionsto one file per module.It uses TypeScript 3.6, the version that built these packages.
With it, the classes stay ES5 classes and the getters stay enumerable, as in the published files.
The first run installs TypeScript 3.6 with
npm ciin thetoolsfolder, so it needs network access.The widget manager packages run the script on
prepare,prebuildandprewatch, sonpm install,npm run buildandnpm run watchrun it.build:libandbuild:proddo not run it.It also stops when its output and the published file have different counts of enumerable or non-enumerable getters.
The app build stops when the split files are missing, or when they were made for another version of the package.
lumino) no longer runs.The page still starts, and the feature loads with what it needs on first use.
luminorow, and says which requirejs modules load it.luminolazy loads.Validation
I built all 8 bundles: Vue 2 and 3, ipywidgets 7 and 8, production and development.
I compared them with the build of #1236 at
6e151ed0.I tested on three environments: Vue 3 with ipywidgets 8, Vue 2 with ipywidgets 8, and Vue 2 with ipywidgets 7.
I built twice, because a code comment changed after the first build.
The browser tests ran on the first build, and the sizes come from the second build.
The production bundles of both builds are byte for byte the same; only the two development core files of each Vue version differ.
Sizes are bytes after
gzip -9 -n."Full JS" is the core plus every chunk that the
fullpreset preloads.luminogzipfullpage loads about 0.4 KB gzip more JS than with perf: let minimal pages without an Output widget skip the Jupyter output renderers #1236, in one more request.The
jupyter-controlsandoutput-widgetchunks grow by less than 30 bytes gzip.fullpage loads 11.3 to 11.6 KB gzip more JS.These two chunks grow by 82 to 138 bytes gzip.
@lumino/widgets(widget,panel,layout,panellayout,title).The
luminochunk has the other 23, and 13 of the 18 module files of@lumino/algorithm.It also has
@lumino/commands,dragdrop,keyboardandvirtualdom.tests/unit/frontend*_test.py(three files) once on each environment: 94 passed on each.The new tests are:
test_lumino_out_of_corechecks for all 8 builds that the core has jQuery and noDockPanel, and that theluminochunk hasDockPaneland no jQuery.test_lazy_load_message_names_the_modulechecks that the server logs one line that namesluminoand the module.test_lazy_load_message_ignores_a_strange_modulechecks that the server leaves out a module name that the browser sent with a newline.tests/integration/frontend_chunks_test.pyonce on each environment, with the flask and the starlette server: 20 passed on each.The new tests are:
test_lumino_requirejs[minimal]: a page widget that asks requirejs for@phosphor/signaling,@lumino/domutilsand@jupyterlab/coreutilsloads noluminochunk and gives no warning.A page widget that asks for
@lumino/widgetsloadsluminoonce, with one browser warning and one server log line that name@lumino/widgets.Its ES5 subclass of
Widgetrenders in aDockPanel, and theWidgetclass of the chunk is the one of the core.test_lumino_requirejs[full]: the same pages, withluminopreloaded and no warning.test_tab[full]andtest_tab[minimal]: theTabwidget switches tabs. Inminimal, the only warning namesjupyter-controls.test_full_lumino_request_fails: the preloadedluminochunk fails once with a 404. The page still starts, and theTab, the Output widget and the page widget all work after the chunk loads on first use.test_full_preloads_sync,test_full_display_outputs,test_minimal_lazy_controls,test_minimal_lazy_outputandtest_full_output_widget_request_fails.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 this Lumino code on Vue 3.
It runs for about 3.6 ms at normal CPU speed and about 16 ms at 4x slowdown.
That chunk also had the rest of
@jupyterlab/coreutils, which stays in the core here.I did not repeat that profile on this branch.
tests/benchmark/summary_test.pyon this branch, although this commit changes it.It passed (7 tests) on the branch of the jquery PR (perf: let minimal pages without jQuery widgets skip loading jQuery (opt-in) #1241), which builds on this one.
test_minimal_soft_remount_keeps_one_widget_rendereron this branch.It passed on the branch of the jquery PR (perf: let minimal pages without jQuery widgets skip loading jQuery (opt-in) #1241).
The browser tests use a page widget that asks requirejs for the same modules.
lumino, because each feature warns once.@lumino/pollingand all of@jupyterlab/coreutils, because Q47 dropped the stand-ins that removed 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, so here they needlumino.semverport 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, andimport reis back infrontend.py(the base had removed it). 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 one commit of this PR (
ed204719) from the commit alone, without builds or tests.gpt-6.1-sol found no defects.
astra found one MEDIUM finding, which is not a defect:
In
minimal, when widgets ask for@lumino/widgetsand then@lumino/commands, only the first module gets a browser warning and a server log line.test_lazy_load_message_names_the_modulechecks it.There were no small fixes.
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