Skip to content

perf: let nodeps.js use the host's Vuetify instead of a second copy - #346

Closed
maartenbreddels wants to merge 4 commits into
masterfrom
perf/nodeps-host-vuetify
Closed

maartenbreddels wants to merge 4 commits into
masterfrom
perf/nodeps-host-vuetify

Conversation

@maartenbreddels

@maartenbreddels maartenbreddels commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

nodeps.js (the bundle Solara loads) now uses the Vuetify of the host page instead of bundling a second copy, which cuts it from 354 KB to 18 KB gzip.

Problem

nodeps.js is meant for hosts that already have Vue and Vuetify, such as Solara.
It still bundled Vuetify's components, directives, and a copy of the Vuetify CSS scoped to .vuetify-styles.
That is 2.6 MB raw and 354 KB gzip that every Solara page downloads, for code and styles the page already has.
The page also ran two Vuetify instances: Solara's, and one for widgets that ipywidgets containers (Lumino) host.

Change

  • VuetifyView.js gets its Vuetify plugin from createVuetifyPlugin() in the new vuetifyPlugin.js.
    For index.js and the labextension, that module is the same createVuetify(...) with styles as before, so Jupyter does not change.
  • For both nodeps.js builds, webpack replaces ./vuetifyPlugin with nodepsVuetifyPlugin.js.
    It returns the host's global vuetifyPlugin, and throws a clear error when the host has none.
    It reads the global instead of the AMD module solara-vuetify-plugin, because Solara 1.23 to 1.60 run Vue 3 but do not define that module (it came in 1.61.0).
    When the host's Vuetify has an older major or minor version than the one ipyvuetify is built for, it logs a console warning that names the versions and says to upgrade Solara.
  • VuetifyApp.js: the DatePicker wrapper resolves VDatePicker from the app it renders in (the host's Vuetify in nodeps.js, ipyvuetify's in Jupyter), instead of importing it.
  • nodepsVuetifyView.js and plugins/nodepsVuetify.js were dead Vue 2 code, so I deleted them.
  • All five AMD builds (extension.js, index.js, both nodeps.js, dist/index.js) now minify with webpack's default Terser settings plus wrap_func_args. Terser 5.43 stopped putting a function argument in parentheses by default. Without them, V8 compiles the AMD factory lazily on the main thread when require.js calls it. With them, V8 compiles it with the script, on a background thread when the script streams. The build adds the devDependency minimizer-webpack-plugin, the Terser plugin that webpack uses for its own default minimizer.
  • CI: the build job fails when either nodeps.js (nbextension or js/dist) is over 250,000 bytes or contains v-data-table. That string is 394 times in the 3.0.0 copy and 0 times in the new one.
    The ui-test job does not check the installed nodeps.js again. CI installs the PR wheel by path, and overlay_css_test.py fails when an older nodeps.js replaces it.
  • New overlay_css_test.py (Solara runner) loads a custom.css with .v-btn { text-transform: none; } and checks a Lumino-hosted button and a button in a menu overlay. It passes with this PR and fails with the 3.0.0 nodeps.js.
  • New tests/ui/conftest.py: on the Solara runner, a test fails on a page error, or on a console message about an older host Vuetify or a component that did not resolve.

Sizes

file before raw / gzip after raw / gzip
nodeps.js 2,594,705 / 353,753 120,837 / 18,148
nodeps.js.map 4,802,166 349,951
index.js 3,722,133 / 473,677 3,726,227 / 474,471

Validation

  • On the Solara runner (released Solara 1.64.0, released ipyvue 3.0.0), I ran each test once with the released 3.0.0 nodeps.js and once with the new one:

    • button_test.py: I made a darwin reference with 3.0.0; the new build matched it. I did not commit that reference.
    • new date_picker_test.py: clicking day 20 sets v_model to 2024-01-20. It passed with both builds.
  • A Solara 1.64.0 app with a custom CSS rule .v-btn { text-transform: none; }, a Lumino-hosted v.Btn(color="primary"), a v-menu, and the dark theme, measured with Playwright:

    button 3.0.0 this PR
    main tree none none
    Lumino-hosted uppercase none
    menu activator and overlay uppercase none

    Background and text colors were the same in both builds, in light and dark mode.
    The page had 126 <style> elements with 3.0.0 and 8 with this PR. The console had no warnings or errors.

  • Solara 1.60.3 (Vuetify 3.3.23) with released 3.0.0: the Lumino-hosted button and the date picker did not render (useId is not a function, Could not find injected date options).
    With this PR, the page renders and does not hang. The date picker stays empty, because Vuetify 3.3 has no VDatePicker, and the console shows the version warning.

  • Wrapper: on a Solara 1.64 page with one ipyvuetify button at 4x CPU slowdown (Playwright Chromium, median of 5 loads per build), the nodeps.js factory call takes 5.0 ms instead of 15.3 ms. The main-thread compile inside it takes 1.5 ms instead of 11.0 ms.

Gaps

  • I did not run the voila, jupyter_lab and jupyter_notebook runners locally; CI runs them. index.js grew by 4,094 bytes (794 gzip). 50 bytes come from the plugin module, and the rest from the wrap_func_args parentheses.
  • Hosts other than Solara that load nodeps.js (for example py.cafe) are not checked. They must set the global vuetifyPlugin and have Vuetify CSS on the page.
  • With Solara before 1.61 (Vuetify 3.3), components that Vuetify 3.3 lacks do not render. Before this PR, those versions failed with errors.
  • When Solara runs without Vuetify (its planned -vuetify mode), AMD vuetify must resolve through a loader that also sets window.vuetifyPlugin first. That is the Solara PR's job.

Visible changes in full (need Maarten's OK)

  • App CSS now beats Vuetify rules of equal specificity inside overlays and Lumino-hosted views, as it already did in the main tree. Before, the scoped Vuetify copy loaded after custom.css and won there (see the table above).
  • Lumino-hosted ipyvuetify views now use Solara's Vuetify instance and theme instead of their own instance. I saw no color difference in light or dark mode, but there is now one writer to vuetify-theme-stylesheet instead of two.

Review

Three reviewers checked this change together with the ipyvue change, each from one angle: template compile semantics, host compatibility (JupyterLab, notebook, Voila, old and new Solara), and build and packaging.
They found no problems in this repository.

This PR is part of the Solara page-load work; the Solara PR that sets the host vuetifyPlugin for this bundle is widgetti/solara#1235.

🤖 Generated with Claude Code

maartenbreddels added a commit to widgetti/solara that referenced this pull request Oct 4, 2026
Vuetify lets an app build its own stylesheet, for example from Vuetify's
SASS with other variables. Such an app does not want Solara's copy of
Vuetify's CSS on the page: it is about 70 KB gzip for nothing, and its rules
fight the app's own rules. Before, there was no way to leave it out: on
Vue 3 it loaded with the vuetify chunk, and on Vue 2 it was part of the core
CSS (main{M}.css).

Vuetify's CSS is now the frontend feature vuetify-css. It comes with
vuetify, as Roboto does, so nothing changes by default, and "-vuetify-css"
leaves it out (for example "full,-vuetify-css" or
"minimal,+vuetify,-vuetify-css"), on Vue 2 and Vue 3, also when Vuetify
loads on first use. A page without Vuetify ("full,-vuetify") gets no
Vuetify CSS, as before.

The file keeps its name, main{M}.vuetify.css, and its place in the page:
on Vue 3 right after the core CSS, where the CSS of the vuetify chunk was,
and on Vue 2 right before the core CSS, where it was inside main{M}.css. On
Vue 3, Vuetify's components import their own CSS, so a small webpack plugin
moves the CSS of the vuetify chunk into the vuetify-css chunk in the same
order; the CSS files are byte-identical to the previous build.

Checked against the previous build in production mode with ipywidgets 8,
full mode, on a page with about 30 kinds of Vuetify widgets: 0 pixels
differ and the computed styles of every element are equal, on Vue 2 and
Vue 3. On Vue 2 the old main8.css and the new main8.vuetify.css plus
main8.css have the same rules in the same order. The Vue 3 full page only
gains "vuetify-css" in its feature list.

Not covered: the theme stylesheet that Vuetify makes in JavaScript stays,
and on Vue 3 the published ipyvuetify 3.0.0 still adds its own copy of
Vuetify's CSS (widgetti/ipyvuetify#346 removes it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
maartenbreddels and others added 4 commits October 6, 2026 10:14
nodeps.js is the bundle for hosts that already have Vue and Vuetify
(Solara). It still bundled Vuetify's components, directives and a scoped
copy of the Vuetify CSS: 2.6 MB raw, 354 KB gzip, for code and styles the
Solara page already has.

nodeps.js now uses the host's Vuetify plugin (the global vuetifyPlugin)
and drops the bundled Vuetify JS, CSS and source maps. It reads the global
instead of the AMD module "solara-vuetify-plugin", because Solara 1.23 to
1.60 run Vue 3 but do not define that module. When the host's Vuetify is
older than the one ipyvuetify is built for, a console warning says so.
DatePicker resolves VDatePicker from the app it renders in. index.js and
the labextension still bundle Vuetify, so Jupyter does not change.
nodepsVuetifyView.js and plugins/nodepsVuetify.js were dead Vue 2 code
with a confusingly similar name, so they go.

nodeps.js goes from 2,594,705 to 120,815 bytes (353,753 to 18,139 gzip).
On the Solara runner the button snapshot matches the 3.0.0 render and the
new date picker test passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in overlays

The PR shrinks nodeps.js from 2.6 MB to 120 KB by using the host's Vuetify,
but nothing failed if a later change bundled Vuetify again. The build job now
fails when nodeps.js (the nbextension copy and js/dist) is over 250,000 bytes
or contains "v-data-table", a string that was in the old copy about 400 times.
The UI test job also checks that the installed nodeps.js is this small build,
because in ipyvue a test dependency once replaced the wheel under test.

The visible change of the PR, app CSS that now wins inside overlays and
Lumino-hosted views, had no test. overlay_css_test.py loads a custom.css in
the page head on the Solara runner and checks that a button in a VBox and a
button in a menu overlay are not uppercase. With the 3.0.0 nodeps.js both
were uppercase and the test fails.

On the Solara runner, a test now fails on a page error or on a console
message about an older host Vuetify or a component that did not resolve.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Terser 5.43 stopped wrapping function arguments in parentheses by default.
The AMD factory that nodeps.js passes to define() is then not wrapped, so
V8 only pre-parses it with the script and compiles it lazily on the main
thread when require.js calls it. Solara had the same problem and fixed it
the same way.

All nbextension and dist builds now use webpack's own Terser settings plus
wrap_func_args, so the factory is define([...],((e,i,o)=>...)) again, and
V8 compiles it with the script, on a background thread when the script
streams.

On a Solara page with one ipyvuetify button at 4x CPU slowdown (median of
5 loads per build), the factory call takes 5.0 ms instead of 15.3 ms, and
the main-thread compile in it is 1.5 ms instead of 11.0 ms. nodeps.js grows
by 22 bytes and index.js by 4 KB (0.8 KB gzip).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The step guarded against a test dependency that replaces the wheel under
test, as once happened in ipyvue. In this repo CI installs the PR wheel by
path, and the run for 2331216 shows that nothing replaced it. If an older
ipyvuetify did replace it, overlay_css_test.py already fails, because the
old nodeps.js makes those buttons uppercase. So the step adds nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels

Copy link
Copy Markdown
Collaborator Author

Replaced by #350, which gets the same nodeps.js win (354 to 18 KB gzip) with a much smaller diff: 5 files instead of 13, no new tests, CI checks or unrelated build changes.

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