Skip to content

fix(playground): defer loading of large emitters until selected - #11508

Merged
timotheeguerin merged 4 commits into
microsoft:mainfrom
timotheeguerin:fix/playground-defer-emitter-load
Jul 31, 2026
Merged

fix(playground): defer loading of large emitters until selected#11508
timotheeguerin merged 4 commits into
microsoft:mainfrom
timotheeguerin:fix/playground-defer-emitter-load

Conversation

@timotheeguerin

@timotheeguerin timotheeguerin commented Jul 31, 2026

Copy link
Copy Markdown
Member

Fixes #11506

Problem

typespec.io/playground hangs forever on iOS. The standalone playground (cadlplayground.z22.web.core.windows.net) works fine.

The difference: website/src/components/playground-component/import-map.ts adds @typespec/http-client-python and @typespec/http-client-csharp to the website's import map, and browser-host.ts eagerly import()s every library in the map at startup — before the user has picked an emitter.

@typespec/http-client-python boots a full Pyodide (CPython/WASM) runtime as a module-level side effect, which alone pulls ~23 MB of extra payload and ~270 MB of resident memory.

Measured with Playwright WebKit under iPhone 15 emulation (ps RSS of the WebKit.WebContent process):

Page RSS
typespec.io home 195 MB
standalone playground (works on real iPhone) 547 MB
typespec.io/playground?version=1.13.x (no extra emitters — works on real iPhone) 463 MB
typespec.io/playground (as shipped — hangs on real iPhone) 741 MB

Fix

Add a deferredEmitters option. Libraries named there are registered as placeholder entries at startup (so they still show up in the emitter dropdown) and are only imported when a compilation actually needs them — i.e. when they're the selected emitter or listed under emit in the tspconfig.

The website passes the two heavy client emitters. Nothing is downloaded or evaluated for them until you select one.

Caveat, documented on the option: a deferred library can't be referenced by an import statement in TypeSpec source. That's fine for these two — they're pure emitters with no decorators or .tsp surface.

Relation to #11507

#11507 makes the Pyodide boot lazy inside the Python emitter. That's the right fix at the source, but the website resolves emitter bundles at runtime from blob storage (typespec.blob.core.windows.net/pkgs/<name>/latest.json), uploaded by a separate ADO stage. So it can't take effect — or be validated in a PR preview — until the emitter is republished. This PR fixes the site independently of that, and also cuts startup cost for the C# emitter.

Tests

  • packages/playground/test/browser-host-deferred.test.ts — deferral, exclusion from the virtual package.json dependencies, load-once memoization, retry after a failed load, no-op for non-deferred libs.
  • packages/playground/test/deferred-emitter-compile.test.ts — end-to-end against the real compiler: compiling with a deferred emitter fails to resolve it, then succeeds and writes its output after loadLibrary().

Full playground suite: 55/55 green. tsc --noEmit clean for packages/playground and website/src.

Not fixed here

Monaco web workers fail to start on typespec.io on desktop and mobile (Could not create web worker(s). Falling back to loading web worker code in main thread) — MonacoEnvironment.getWorker isn't defined. Pre-existing and unrelated to the hang, but worth a follow-up: everything currently runs on the main thread.

Why this is an opt-in list rather than "defer every emitter"

isEmitter comes from $lib.emitter, which only exists once the bundle has been evaluated. Neither the blob index (latest.json is just {version, imports}) nor package.json carries an emitter marker, so there is no way to populate the emitter dropdown without importing every candidate. Deferring everything would leave the dropdown empty.

Deferring all emitters would also break the ones that ship TypeSpec surface — @typespec/openapi3 (lib/decorators.tsp), @typespec/json-schema, @typespec/protobuf — since an import "@typespec/json-schema"; in the editor would fail to resolve before the emitter is ever selected. http-client-python and http-client-csharp are the only two in the map that are pure emitters with no .tsp.

The payoff is also concentrated in those two: with pyodide blocked the page sits at 474 MB vs 463 MB for ?version=1.13.x (which loads no additional packages at all), so everything else the website pulls in eagerly — including the ~7 Azure libraries that http-client-python's import map drags along — is only ~11 MB combined.

Possible follow-up, not done here: the host's readFile/stat are async, so a miss under node_modules/<deferred-lib>/ could trigger the load mid-resolution. That would remove the "cannot be referenced by an import statement" restriction and make the list safe to expand without auditing each library.

The playground eagerly imported every library in its import map on startup.
On typespec.io that includes `@typespec/http-client-python` and
`@typespec/http-client-csharp`, which are large enough to push iOS Safari
past its per-tab memory budget, so the page never finished loading.

Emitters listed in the new `deferredEmitters` option are now registered as
placeholders and only imported when they are actually used for a
compilation.
@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/playground@11508

commit: b8585d9

@github-actions

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/playground
Show changes

@typespec/playground - fix ✏️

Add support for deferring the loading of emitter libraries until they are selected. Configure with the new deferredEmitters option to avoid downloading and evaluating large emitters on startup.

@azure-sdk-automation

azure-sdk-automation Bot commented Jul 31, 2026

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

…ocking

The suite runs with `isolate: false`, so `vi.mock("../src/core.js")` was not
reliably applied when the whole workspace runs in one vitest instance and the
real `importLibrary` was used instead.

Test the injectable seams directly instead: `createBrowserHostInternal` takes
the loaders, and the eager/deferred split is now a pure `splitDeferredLibraries`
helper. No module mocking is involved.

Also fix two portability bugs in the compile test: `join()` was used to build
virtual FS paths, which produces backslashes on Windows, and
`import.meta.resolve` is replaced with `createRequire`.
@timotheeguerin
timotheeguerin marked this pull request as ready for review July 31, 2026 19:08
@timotheeguerin
timotheeguerin added this pull request to the merge queue Jul 31, 2026
Merged via the queue into microsoft:main with commit 0e55c24 Jul 31, 2026
33 checks passed
@timotheeguerin
timotheeguerin deleted the fix/playground-defer-emitter-load branch July 31, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Playground doesn't seem to load on mobile

2 participants