Repository navigation
perf: load the SFC compiler as a lazy chunk, and TypeScript support only when a template uses it - #114
Merged
Conversation
maartenbreddels
force-pushed
the
perf/lazy-sfc-chunk
branch
from
October 2, 2026 23:53
6654683 to
253d7a9
Compare
maartenbreddels
force-pushed
the
perf/lazy-sfc-chunk
branch
from
October 4, 2026 10:19
253d7a9 to
86e6b42
Compare
Every page with jupyter-vue downloaded @vue/compiler-sfc and sucrase before its first render. In Solara, nodeps.js was 326 KB gzip, and every app waited for it. These two libraries are now in a lazy chunk, vue-sfc, and nodeps.js is about 44 KB gzip. Templates compile exactly as in ipyvue 3.0.0. The compile code moved unchanged into the chunk, and the first template on a page loads it. After a failed load, the next template tries again. The es-module-shims runtime and addModule stay in the main bundle, so they do not wait for the chunk. The chunk file has a stable name (nodeps-vue-sfc.js, index-vue-sfc.js) and data-webpack key (jupyter-vue-nodeps:chunk-vue-sfc), so a host such as Solara can preload it with a script tag. Before the load, jupyter-vue sends the window event jupyter-vue:load-chunk, so a host can tell the user how to preload it. The AMD builds load the chunk from the folder of the file that requirejs loaded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only a template with <script lang="ts"> needs sucrase. With sucrase in the vue-sfc chunk, every page that compiles a template paid for it. Every Solara page compiles a template (navigator.vue), so every Solara page downloaded and evaluated sucrase without using it. sfcCompiler.js now imports sucrase with a dynamic import in the lang="ts" branch only, and calls the same transform with the same options. Webpack puts it in its own chunk, vue-sfc-ts, with stable file names (nodeps-vue-sfc-ts.js, index-vue-sfc-ts.js), so a host can preload it like vue-sfc. The CI build guard checks that sucrase is only in that chunk, and the UI test checks that a plain template does not request it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
VueTemplateRenderer.js imported all of lodash to call cloneDeep and isEqual, so webpack put the whole library in nodeps.js and index.js, which every page loads. It now imports lodash/cloneDeep and lodash/isEqual, the same functions, so only those two and their helpers are in the bundle. No other file imports lodash. In a local production build, nodeps.js went from 126,328 to 75,759 bytes (43,884 to 25,267 gzip -9), and index.js from 324,491 to 273,882. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… build The UI job checks that the vue-sfc chunk is installed after it reinstalls this wheel. Without that check, a wheel replaced by a released ipyvue (which has no lazy chunk) makes the chunk tests skip or fail for a reason that is hard to see. In the labextension, @jupyterlab/builder shares every dependency, so sucrase was a shared module. webpack keeps a failed shared install, so after one failed download every later TypeScript template failed until a reload. "sucrase": false in jupyterlab.sharedPackages makes it a normal lazy chunk (vue-sfc-ts) in Lab too, and webpack retries a normal chunk. A new UI test aborts the first vue-sfc-ts request and checks that a later TypeScript template renders. Validation: one bdist_wheel build. remoteEntry no longer registers sucrase as shared, and static/vue-sfc-ts.<hash>.js holds sucrase. With this wheel in a copy of the CI venv (Python 3.11), pytest tests/ui/ on all four runners: 83 passed, 1 skipped (the vue-sfc retry test on the solara runner, as designed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
setup.py checks that the built files exist after npm run build. It named the compiler chunk but not the new TypeScript chunk, so a build without nodeps-vue-sfc-ts.js would still pass. Hosts may preload that file by name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nbextension.js had the same two statements as embed.js: import publicPath.js, then re-export index.js. Both AMD bundles now need the same thing, the chunk path from the folder of the loaded file, so one entry file is enough and the PR adds one file less. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The separate test rendered PLAIN again only to check that a plain template does not request the vue-sfc-ts chunk. The plain-template test already renders PLAIN and two more templates, so it can make that check. This keeps the coverage and saves one kernel and page run per runner. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…path An earlier version of this branch compiled plain templates with Vue.compile and sent names that start with _ to the full compiler. This test guarded that switch. Now every template uses the same compile code as 3.0.0, so the test checks nothing that this change can break. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Importing all of lodash has a side effect: webpack folds lodash's AMD check to true, so the bundle sets window._ to lodash. Solara uses that global (_.escape, _.debounce, _.isEqual), and user templates may too. Importing only cloneDeep and isEqual removed the global. That change is not needed for the lazy SFC chunk, so it belongs in its own PR that also deals with the global. This reverts commit 86e6b42. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
maartenbreddels
force-pushed
the
perf/lazy-sfc-chunk
branch
from
October 5, 2026 16:55
956f524 to
c511a3b
Compare
maartenbreddels
marked this pull request as ready for review
October 5, 2026 18:18
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.
nodeps.jsshrinks from 326 to 44 KB gzip: the SFC compiler moves to a lazy chunk that hosts can preload, TypeScript support loads only when a template uses it, and every template compiles exactly as in 3.0.0.Problem
Every page that uses jupyter-vue downloads
@vue/compiler-sfcandsucraseinsidenodeps.js(Solara) orindex.js(classic notebook) before jupyter-vue can start: 326 KB gzip fornodeps.js.Pages without any template pay for the compiler anyway.
sucraseonly strips TypeScript, but every page downloads it, also when no template useslang="ts".Change
js/src/sfcCompiler.js(new) is thevue-sfcchunk:@vue/compiler-sfcand the compile code.It keeps 3.0.0's compile code from
esmVueTemplate.js. The only change in it is that sucrase now comes from an awaited dynamic import (see below), with the sametransformcall and options.Every template, plain or
<script setup>, compiles with the same code as in 3.0.0.js/src/esmVueTemplate.jsloads the chunk withimport()on the first template.Before the load, it dispatches the window event
jupyter-vue:load-chunkwith{chunk: 'vue-sfc', sourceURL}, so a host can tell the user how to preload it.After a failed load, the next template tries again.
The es-module-shims runtime stays in the main bundle, so
addModuledoes not wait for the chunk.sucrasemoves to a second lazy chunk,vue-sfc-ts, which loads only for<script lang="ts">, with the sametransformcall and options.js/src/publicPath.js(new) makes the AMD builds load the chunks from the folder of the file that requirejs loaded, with the same query string (the cache-busting hash of Solara or the notebook).index.js) is now built fromjs/src/embed.js, which importspublicPath.jsfirst, as the CDN embed bundle already did.setup.pychecks after the npm build that both chunk files exist (nodeps-vue-sfc.js,nodeps-vue-sfc-ts.js).js/webpack.config.js: stable chunk file names (nodeps-vue-sfc.js,nodeps-vue-sfc-ts.js,index-vue-sfc.js,index-vue-sfc-ts.js), oneuniqueNameper build, named chunk ids, no split chunks. The nbextension builds set a staticpublicPath, whichpublicPath.jsreplaces. In the labextension build, the chunks are normal federated chunks.compileScriptis only in thevue-sfcchunks, and that sucrase is only in thevue-sfc-tschunks, so the bundles cannot silently grow back.ipyvuetify 3.0.0a3, which requiresipyvue 3.0.0a5by URL and replaced the built wheel, so the UI tests ran against 3.0.0a5 (also on master). The job now reinstalls the wheel last and fails when the chunk file is missing.sucraseis no longer a shared module ("sucrase": falseinjupyterlab.sharedPackages). As a shared module, one failed download would fail every later TypeScript template until a reload, because webpack keeps a failed shared install. A normal chunk retries.Host contract (Solara)
To preload the compiler chunk, a host adds this tag before the chunk is needed:
A host can preload the TypeScript chunk the same way, for pages with
<script lang="ts">templates:webpack then reuses the tag, and the chunk is requested once.
The
onerrorattribute is needed: webpack also reuses a tag that already failed, and then waits 120 s for it.Hosts that serve
nodeps.jsthemselves must also serve the chunk files next to it.solara#1235 adds this tag in its default mode.
Sizes
nodeps.js(Solara)nodeps-vue-sfc.js(lazy, compiler)nodeps-vue-sfc-ts.js(lazy, TypeScript)index.js(classic notebook)index-vue-sfc.js(lazy, compiler)index-vue-sfc-ts.js(lazy, TypeScript)gzip sizes are from
gzip -9.A page without templates downloads 44 KB gzip (was 326), a page with templates 283 KB, and a page with a TypeScript template 328 KB.
Every Solara page renders a template (
navigator.vue), so on Solara the compiler chunk always loads; Solara can preload it in parallel with the rest of the page.Validation
<script setup lang="ts">with a scoped style;export defaultwith scoped and unscoped styles;v-bindobjects on Vuetify activators; watchers; a trait and a computed with the same name;v-modelon a prop; comments; a global component; a template without a script; a template change at run time; a_namemethod; a broken template.The DOM before and after clicks and typing, the overlay HTML, the
<style>contents and the console messages were identical.test_sfc_chunk.py(a TypeScript template loads both chunks; plain templates load the compiler chunk once and never the TypeScript chunk),test_template.py,test_v_bind.py,test_v_on.pypass. The retry test (a failed chunk load, then a template that renders) runs on the notebook runners; on Solara it skips, becausenavigator.vueloads the chunk before the test starts.pytest tests/ui/with this wheel, as CI runs it: 83 passed, 1 skipped (the compiler retry test skips on Solara, becausenavigator.vueloads the chunk first; it runs on Voila, JupyterLab and the notebook). A new test aborts the first TypeScript chunk download and checks that a later TypeScript template renders, on all 4 runners.sucraseretry problem, which is fixed; nothing else was found.Gaps
tests/ui/test_watchers.pycannot fail as written (they never wait for their last locator); this PR does not change them.window._as a side effect, and Solara uses that global (_.escape,_.debounce,_.isEqual). An earlier commit imported onlycloneDeepandisEqual, which removed the global, so it was reverted. A separate PR can remove lodash together with the global, and save 19 KB gzip more.This PR is part of the Solara page-load work; the Solara PR that preloads the chunk is widgetti/solara#1235.
🤖 Generated with Claude Code