Skip to content

perf: stop old core-js from slowing down regex on every Vue 2 page, and repair 1.x CI - #347

Merged
maartenbreddels merged 4 commits into
1.xfrom
perf/corejs-1x
Oct 5, 2026
Merged

maartenbreddels merged 4 commits into
1.xfrom
perf/corejs-1x

Conversation

@maartenbreddels

@maartenbreddels maartenbreddels commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

On the 1.x line, update core-js from 3.0.1 to 3.50.0. After this change, loading jupyter-vuetify no longer makes every regex split, match and replace on the page up to 80 times slower in Chrome and Edge. This PR also repairs CI on 1.x.

Problem

Every page that loads jupyter-vuetify 1.x in Chrome or Edge gets slow regex work everywhere, not only in jupyter-vuetify.
Vue 2 apps (Solara with Vue 2, Voila and notebooks with ipyvuetify 1.x) pay for this on every page load.

The cause is core-js 3.0.1 in the bundles.
Its feature check for String#split writes constructor on a real RegExp.
In V8, that write turns off the RegExp fast path for the whole page.
After that, String#split, #match, #replace and #search with a RegExp take the slow path, also in Vue's template compiler and in other libraries.
In Chromium 133, after jupyter-vuetify 1.11.3's nodeps.js loads, split(/\r?\n/g) on a 729 KB string takes 41 ms instead of 0.5 ms.
Global match and replace get 4 to 7 times slower.

core-js fixed this check in 3.3.4 (zloirock/core-js#306): it now uses a plain object.
jupyter-vue 1.12.0 has the same problem (core-js 3.1.4), so both packages need the fix.

CI on the 1.x branch is broken: the build job installs ipyvue without a version and gets ipyvue 3.0.0.
With it, python -m ipyvuetify.components fails with KeyError: TraitNotAvailable in reacton's generator.
The build job also runs on Node 16.
The build backend installs the newest jupyterlab 4.x, and since jupyterlab 4.6.0 the labextension build stops on Node older than 20.19.0.
Then python -m build fails with No module named 'generate_source', but that error is only a result of the first one: without the labextension build, webpack never writes the nbextension, and the sdist does not ship the generator that would rebuild it.

Change

  • js/package.json and js/package-lock.json: core-js 3.0.1 to 3.50.0 (^3.50.0).
    The babel config does not change, so babel injects the same polyfill modules and the browser support stays the same.
    npm keeps a nested core-js 3.0.1 under the dev dependency core-js-compat. It runs only at build time, and the scan below shows that no bundle ships it.
    nodeps.js grows by 3.3 KB gzip (50.4 to 53.8 KB).
  • CI: the build job installs "ipyvue>=1.7,<2", the same range that the package requires.
  • CI: the build job uses Node 22 instead of Node 16, because the labextension builder of jupyterlab 4.6 needs Node.js ^20.19.0 || >=22.12.0.
    The release jobs only run npm publish and keep Node 14.
  • CI: releases from this branch publish to npm with the dist-tag latest-1, not latest.
    npm's latest for jupyter-vuetify is 3.0.0, and a 1.x release with latest would move it back to 1.x.
    npm rejects a tag such as 1.x, because it looks like a semver range.
    The old pre-release switch is removed: it read a release-event field, and this workflow never runs on release events.
  • CI: the build job scans every shipped bundle (prefix/share/jupyter/nbextensions, prefix/share/jupyter/labextensions/.../static, js/dist) for the core-js version marker.
    It fails when it finds a version below 3.3.5, so an old core-js cannot come back through a lockfile change.
    The script .github/check_corejs.py uses only the Python standard library, so the build job needs no extra install.

Validation

A page-load harness measured a large production Vue 2 Solara app, with synthetic data (solara 1.63.1, ipyvue 1.12.0, ipyvuetify 1.11.3).
It compared stock with the same core-js bump in both ipyvue and ipyvuetify.
The jupyter-vuetify nodeps.js in that test has the same sha256 (e8d0627b...) as the build of this branch.
The metric is the time to the first useful view, cold cache, p50 of 20 loads per cell:

page CPU stock (ms) fixed (ms) difference [95% CI]
home 1x 1033 948 -85 [-104, -64]
home 4x 2406 2038 -368 [-415, -338]
dashboard 1x 1288 1267 -21 [-43, +9]
dashboard 4x 2531 2464 -67 [-146, -8]

In the same loads, a 700 KB split(/\r?\n/g) after the load took 35 ms (1x) and 152 ms (4x) with stock, and 0.3 ms and 1.1 ms with the fix.
The DOM was the same, and no arm had console errors.

With only ipyvue fixed, the page stays slow. In a test page with 182 large templates (Playwright Chromium 133, 1x CPU), the split still took 42 ms, because jupyter-vuetify's core-js turned off the fast path.

Polyfills: the babel output is byte-identical before and after, because the lockfile change touches only core-js.
No core-js module from the old bundle is missing in the new one.
In Chromium 133, no native method is replaced.
In a real Solara page, an ipyvuetify Btn and a TextField with v_model work with the fixed bundles, without console errors.

On this branch (local run on macOS with node 26; CI now uses node 22):

  • python -m build and npm pack, as in CI.
    The scan finds core-js 3.50.0 in 5 of 14 bundles and no core-js in the other 9, and exits 0.
    On the stock 1.11.3 nodeps.js, it reports core-js 3.0.1 and exits 1.
  • python -m build and npm pack again with node 22.23.3, in a fresh clone of this branch: both pass, and the scan again finds core-js 3.50.0 in 5 of 14 bundles and exits 0.
    With node 16, the CI run before the Node fix failed at jupyter labextension build with "requires Node.js ^20.19.0 || >=22.12.0 (found v16.20.2)".
  • The "Build component file" step: with ipyvue 3.0.0 it exits 1 (KeyError: TraitNotAvailable). With "ipyvue>=1.7,<2" (1.12.0) it exits 0, and components.py does not change.
  • tests/ui/button_test.py with the Solara runner (solara 1.64.0, ipyvue 1.x with the core-js fix, ipywidgets 8.1.9): passed.
    Its button screenshot looks the same as with stock 1.11.3 (largest channel difference 2 of 255).
  • The repository has no unit tests for 1.x.

Gaps

  • Firefox and Safari were not tested. The fast path is V8 only, but the feature checks run in every browser.
  • conda-forge needs a new 1.x build after the release.
  • ipyvue 1.x needs the same fix: perf: stop old core-js from slowing down regex on every Vue 2 page ipyvue#115. With only one of the two packages fixed, Chrome stays slow.
  • The Jupyter Lab, Notebook and Voila runners were not run locally. CI runs them.
  • Read the Docs fails on this PR with a config error: .readthedocs.yaml asks for build.os: ubuntu-20.04, which Read the Docs no longer accepts.
    This PR does not change that file, and 1.x has the same file, so a docs build of 1.x fails the same way.
    The same file also asks for Node 16, so after an OS fix the docs install would probably hit the Node error above. Nobody tested that.
  • The local build patched js/postcss.config.js for the build only, because the check for ipyvuetify/js/lib/styles.css depends on the folder name. The CI checkout has the right folder name, so CI needs no patch.

Release note

Vue 2 pages in Chrome and Edge no longer get slow regex work after jupyter-vuetify loads. Upgrade both packages:

pip install "ipyvue>=1.12.1,<2" "ipyvuetify>=1.11.4,<2"

🤖 Generated with Claude Code

maartenbreddels and others added 2 commits October 3, 2026 14:49
…ace on the whole page

core-js 3.0 runs a feature check for String#split that writes
`constructor` on a real RegExp. In V8 that write turns off the RegExp
species protector for the whole page, so every String#split, #match,
#replace and #search with a RegExp takes the slow path from then on,
also in Vue's template compiler and in other libraries on the page.
In Chromium 133, after nodeps.js loads, split(/\r?\n/g) on 729 KB goes
from 0.5 ms to 41 ms, and global match/replace get 4-6x slower.

core-js 3.3.4 changed this check to use a plain object
(zloirock/core-js#306). Babel still injects the same core-js modules
(the babel config does not change), so the browser support stays the
same. nodeps.js grows by about 10 KB, which is just over webpack's
244 KiB size hint, so webpack now prints size warnings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The build job installed ipyvue without a version, so it now gets ipyvue
3.0.0. With it, `python -m ipyvuetify.components` fails with
KeyError: TraitNotAvailable in reacton's generator, and the build job
fails before any test runs. The package itself already requires
"ipyvue>=1.7,<2"; the build job now uses the same range. With the pin,
the step exits 0 and components.py does not change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
maartenbreddels and others added 2 commits October 4, 2026 14:26
The release jobs published every tag with the npm dist-tag "latest".
npm's latest for jupyter-vuetify is 3.0.0, so a 1.x patch release would
move it back to 1.x, and `npm install jupyter-vuetify` and unpkg URLs
without a version would get the old major. Releases from this branch
now use the dist-tag "latest-1". npm rejects tags that look like a
semver range, such as "1.x". The pre-release switch is gone: it read an
input that only release events set, and release events do not trigger
this workflow.

core-js before 3.3.5 runs feature checks that write `constructor` on a
real RegExp. In Chrome and Edge that turns off V8's fast paths for
String#split, #match, #replace and #search on the whole page.
jupyter-vuetify 1.11.3 shipped core-js 3.0.1 this way. A lockfile change
or a new dependency can bring an old core-js back without anyone
noticing, so the build job now scans every shipped bundle (nbextension,
labextension, js/dist) for the core-js version marker and fails below
3.3.5. The script uses only the standard library, so the build job
needs no extra install.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The build backend installs "jupyterlab~=4.0", so CI gets the newest 4.x.
Since jupyterlab 4.6.0, `jupyter labextension build` goes through
jupyter-builder, which stops on Node.js older than 20.19.0. The build
job used Node 16, so the labextension build failed and webpack never
wrote the nbextension. The sdist then had no built JS, and building the
wheel from it failed with "No module named 'generate_source'", because
the sdist does not ship the generator. The same failure happens on 1.x
without this branch.

The Node 16 pin worked around an OpenSSL error in webpack 4. The 1.x
line has built with webpack 5 since early 2025, and webpack 5 runs on
Node 22.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maartenbreddels
maartenbreddels marked this pull request as ready for review October 5, 2026 08:15
@maartenbreddels
maartenbreddels merged commit 526936b into 1.x Oct 5, 2026
9 of 10 checks passed
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