Skip to content

perf(sdk): load js-yaml, semver and the uipack renderer on first use under require() - #935

Merged
alexmercerpo merged 1 commit into
perf/1.10.x-protocol-static-importsfrom
perf/1.10.x-lazy-optional-deps
Oct 21, 2026
Merged

alexmercerpo merged 1 commit into
perf/1.10.x-protocol-static-importsfrom
perf/1.10.x-lazy-optional-deps

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Stack 2 of 4, based on #934 (perf/1.10.x-protocol-static-imports). Merge in order.

What and why

Every server loaded three packages at startup for code most servers never run:

  • js-yaml (25 files), for parsing SKILL.md front matter and YAML config files (skill-md-parser.ts, sep-2640.builders.ts, the config plugin's config-loader.ts).
  • semver (23 files since perf(sdk): import only the semver functions the ESM loader uses #925), for the ESM loader's version ranges (esm-loader/semver.utils.ts).
  • The @frontmcp/uipack root, for the custom auth-UI pages (auth-ui.render.ts). The OAuth flows import that module, so it loaded with them, in public mode too.

They now come through three subpath imports in libs/sdk/package.json: #js-yaml, #semver and #uipack-render. Each has two variants in libs/sdk/src/third-party/, as @frontmcp/protocol's #client does:

  • The require variant (lazy-*.ts) defines each function as a getter that requires its package the first time it is read.
  • Every other condition gets the static variant, which re-exports the functions.

The CommonJS build keeps the three specifiers external and builds both variants as entry points, so a CommonJS process resolves the lazy one at run time. The ESM and browser builds resolve them at build time to the static variant, so those bundles import js-yaml, semver and @frontmcp/uipack as they did before. tsconfig.base.json, jest.imports-mapper.js and the module-boundaries rule list the three specifiers like the existing ones.

The benchmark's HTTP server loads 404 modules instead of 450, and its stdio server 280 instead of 326. A server that parses a SKILL.md file, uses Tool.esm()/App.esm() with a version range, or serves an auth-UI page loads the package when it does that, as before.

Results

Apple M4 Pro (14 cores, 48 GiB), macOS 15.6.1, Node.js 24.18.1, on AC power, with the machine's normal background load (an IDE and other jobs). The server is the benchmark's FrontMCP implementation (frontmcp build --target node), run with node and no flags, against npm packed builds of this branch and its base, installed into scratch copies of the implementation and built with npm run build, measured in one session in randomized, interleaved rounds with the benchmark's own harness functions. Cold start is the time from spawning node to the first tools/call response, initialize included: one discarded start per server, then 8 rounds of 4 starts per transport in random order. The 100-tool startup is spawn to the first full tools/list, 5 rounds of 2 starts, and its RSS is read 1 s after that. Throughput is the benchmark's closed-loop tools/call at concurrency 1, 16 and 64 (3 s warmup, 10 s measured, 5 rounds); RSS after startup is read 1 s after the first call, RSS after load after the last level, median of the 5 rounds. Heap used is process.memoryUsage().heapUsed 700 ms after start (after the first call, no forced GC), median of 10 interleaved starts per transport in a separate run right after. MB is 10^6 bytes. The 1-minute load average was 3.9 to 7.6 during the cold-start and 100-tool runs, and 6.9 to 9.6 during the heap runs. During the throughput rounds an outside job pushed it as high as 47, and two of the five rounds of each branch ran slower; the medians below are of all five.

perf/1.10.x-protocol-static-imports This PR
Cold start, stdio, p50 / p90 198.7 / 207.4 ms 190.6 / 197.6 ms
Cold start, HTTP, p50 / p90 267.1 / 275.5 ms 257.0 / 278.0 ms
100 tools, spawn to first full tools/list, p50 293.9 ms 270.5 ms
RSS after startup (HTTP) 147.4 MB 141.1 MB
RSS after startup, 100-tool server 152.2 MB 142.2 MB
Heap used after startup, HTTP 39.5 MB 38.3 MB
Heap used after startup, stdio 29.9 MB 28.7 MB
RSS after startup, stdio (read in-process) 124.0 MB 122.8 MB
RSS after load (HTTP) 462.4 MB 478.8 MB
Throughput, c=1 / 16 / 64 5,667 / 8,603 / 8,607 rps 6,034 / 9,049 / 8,889 rps

Per round, the median cold start moved by -7.8 ms over stdio (from -13.8 to +0.9 ms) and by -4.0 ms over HTTP (from -13.4 to +18.9 ms). The change doesn't touch the request path: throughput and RSS after load are within their run-to-run spread (c=64: 7,706 to 8,855 rps for the base, 5,238 to 9,012 for this PR; RSS after load 433 to 475 MB and 421 to 483 MB).

Tests

  • New unit tests for each require variant (src/third-party/__tests__/lazy-*.spec.ts), modeled on @frontmcp/protocol's lazy-client.spec.ts: requiring the module doesn't load the package, reading a function loads it once, and the exports are the same functions the static variant exports.
  • nx run sdk:test --coverage: 7,139 tests in 558 suites pass, among them the SKILL.md parser, the config loader, the ESM loader and the auth-UI specs, which run through the static variant (jest.imports-mapper.js).
  • nx run sdk:lint, nx run sdk:typecheck, node --test scripts/*.test.mjs (84 pass, among them the browser-build and worker-condition guards) and Prettier on the changed files pass.
  • e2e: skills 188 (2 skipped), config 99, esm 26, local-auth 73, bundle-size 8, browser-bundle (Playwright) 11, cloudflare 16 (5 skipped), standalone 49, cli-exec 198 and stdio-transport 26 pass.
  • The built dist/package.json maps each specifier's require condition to ./third-party/lazy-*.js and its other conditions to the static file; dist/index.js requires the three specifiers, and dist/esm/index.mjs and dist/browser/index.cjs import js-yaml directly, as before.
  • Loaded modules, recorded with a module.registerHooks preload: the benchmark's HTTP server loads 404 modules instead of 450, its stdio server 280 instead of 326; what goes is js-yaml, semver and the @frontmcp/uipack root, and three small files from @frontmcp/sdk/third-party come in.
  • The benchmark's correctness gate (HTTP, stdio and the 100-tool server) passes against this build.

Summary by CodeRabbit

  • New Features

    • Added SDK support for YAML parsing, semantic-version utilities, and UI rendering integrations.
    • Added eager and on-demand loading options for these integrations, helping applications load dependencies only when needed.
    • Improved compatibility across supported module systems and SDK build environments.
  • Tests

    • Added coverage confirming deferred loading, exposed APIs, and behavior for YAML, semantic-version, and UI rendering integrations.

…under require()

A server loaded js-yaml (25 files), semver (23) and the @frontmcp/uipack root
at startup, for code most servers never run: SKILL.md and YAML config parsing,
the ESM loader's version ranges, and the auth-UI pages.

They now come through three subpath imports, #js-yaml, #semver and
#uipack-render, whose `require` variant reads each function from its package
the first time it is used, like @frontmcp/protocol's #client. The CommonJS
build keeps them external and ships both variants; the ESM and browser builds
resolve them at build time to the static variant, so those bundles import the
packages as before.
@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@github-actions

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-10-21T15:32:48.723Z
Commit: 11ca4ef7

@frontegg-david
frontegg-david added this pull request to stack #936 October 21, 2026 15:37
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: agentfront/frontmcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 61b1fff1-8696-4b00-b19f-f8a2396f4554
📥 Commits

Reviewing files that changed from the base of the PR and between 9aed2a5 and bc56dbf.

📒 Files selected for processing: 19
  • eslint.config.mjs
  • jest.imports-mapper.js
  • libs/sdk/package.json
  • libs/sdk/project.json
  • libs/sdk/src/auth/auth-ui/auth-ui.render.ts
  • libs/sdk/src/builtin/config/providers/config-loader.ts
  • libs/sdk/src/esm-loader/semver.utils.ts
  • libs/sdk/src/skill/sep-2640/sep-2640.builders.ts
  • libs/sdk/src/skill/skill-md-parser.ts
  • libs/sdk/src/third-party/__tests__/lazy-js-yaml.spec.ts
  • libs/sdk/src/third-party/__tests__/lazy-semver.spec.ts
  • libs/sdk/src/third-party/__tests__/lazy-uipack-render.spec.ts
  • libs/sdk/src/third-party/js-yaml.ts
  • libs/sdk/src/third-party/lazy-js-yaml.ts
  • libs/sdk/src/third-party/lazy-semver.ts
  • libs/sdk/src/third-party/lazy-uipack-render.ts
  • libs/sdk/src/third-party/semver.ts
  • libs/sdk/src/third-party/uipack-render.ts
  • tsconfig.base.json

Included review availability: This review used your included allowance. 3 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.

📝 Walkthrough

The SDK adds eager and lazy adapters for js-yaml, semver, and @frontmcp/uipack. It configures package aliases and build mappings for those adapters, then updates existing SDK imports to use the aliases.

Changes: SDK third-party import aliases

Layer / File(s) Summary
Third-party adapters and lazy loading
libs/sdk/src/third-party/*.ts, libs/sdk/src/third-party/__tests__/*
Adds eager exports for selected YAML, semver, and UI-pack functions. Lazy CommonJS exports defer dependency loading until an export is accessed. Tests check deferred loading and API parity.
Alias and build wiring
libs/sdk/package.json, tsconfig.base.json, libs/sdk/project.json, jest.imports-mapper.js, eslint.config.mjs
Adds the three package aliases to package, TypeScript, Jest, and build configuration. The module-boundary allowlist includes #uipack-render.
SDK consumer imports
libs/sdk/src/builtin/config/providers/config-loader.ts, libs/sdk/src/skill/sep-2640/sep-2640.builders.ts, libs/sdk/src/skill/skill-md-parser.ts, libs/sdk/src/esm-loader/semver.utils.ts, libs/sdk/src/auth/auth-ui/auth-ui.render.ts
Updates YAML and semver imports to use the aliases. Separates UI-pack type imports from runtime imports.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant SdkConsumer
  participant PackageImports
  participant LazyJsYaml
  participant JsYaml
  SdkConsumer->>PackageImports: imports #js-yaml
  PackageImports->>LazyJsYaml: resolves CommonJS target
  SdkConsumer->>LazyJsYaml: reads load export
  LazyJsYaml->>JsYaml: requires js-yaml
  JsYaml-->>LazyJsYaml: provides load
  LazyJsYaml-->>SdkConsumer: returns load
Loading
Merge Risk: ⚪ Minimal · up to bc56d

No actionable merge-blocking issue was established. The change is mergeable after normal build and test checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title "perf(sdk): load js-yaml, semver and the uipack renderer on first use under require()" matches the main change. The PR defers loading of js-yaml, semver, and the @frontmcp/uipack root until …
Description check Passed The description follows the repository template. It includes the 'What and why' section with the change, motivation, and build behavior. It includes a 'Tests' section with unit tests, the SDK test, li…
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "perf(sdk): load js-yaml, semver and the uipack renderer on first use under require()" matches the main change. The PR defers loading of js-yaml, semver, and the @frontmcp/uipack root until first use under CommonJS require(), using new #js-yaml, #semver, and #uipack-render subpath imports, while ESM and browser builds keep static imports. The title is specific, uses a conventional prefix, and is one sentence.

Full details: Description check

Explanation

The description follows the repository template. It includes the 'What and why' section with the change, motivation, and build behavior. It includes a 'Tests' section with unit tests, the SDK test, lint, typecheck, e2e results, and loaded-module measurements. The 'User-facing change' section is omitted, which is acceptable because the template says to delete it when users will not see the change. The author states no public API, option, default, or error message changes; the 'What changed', 'Before → after', 'frontmcp.dev pages affected', and 'Skills catalog entries updated' fields are not present, which is consistent with deletion of that section. The description is detailed and includes benchmark methodology and results.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 16 files. (3 skipped: 3 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@alexmercerpo
alexmercerpo merged commit b7ee831 into release/1.10.x Oct 21, 2026
44 checks passed
@alexmercerpo
alexmercerpo deleted the perf/1.10.x-lazy-optional-deps branch October 21, 2026 16:12
@github-actions

Copy link
Copy Markdown
Contributor

Cherry-pick Created

A cherry-pick PR to main has been automatically created.

Please review and merge if this change should also be in main.

If the cherry-pick is not needed, close the PR.

alexmercerpo pushed a commit that referenced this pull request Oct 21, 2026
* perf(sdk): serve in-memory, direct-client and web-standard MCP with the loaded protocol module (#934)

createInMemoryServer(), the direct client's notification and elicitation
handlers, and runWebStandardMcp() took their classes and schemas from
`await import('@frontmcp/protocol')`. In a CommonJS process that import
resolves the package's `import` condition, so connect(), create() clients and
createFetchHandler() loaded a second, ESM copy of @frontmcp/protocol and,
through it, of the MCP SDK, on top of the CommonJS copy the SDK had already
required.

They now use the statically imported @frontmcp/protocol, as runStdio() does
since #923. In an ESM process the import resolves the same ESM module as before.

(cherry picked from commit 5b54191)

* perf(sdk): load js-yaml, semver and the uipack renderer on first use under require() (#935)

A server loaded js-yaml (25 files), semver (23) and the @frontmcp/uipack root
at startup, for code most servers never run: SKILL.md and YAML config parsing,
the ESM loader's version ranges, and the auth-UI pages.

They now come through three subpath imports, #js-yaml, #semver and
#uipack-render, whose `require` variant reads each function from its package
the first time it is used, like @frontmcp/protocol's #client. The CommonJS
build keeps them external and ships both variants; the ESM and browser builds
resolve them at build time to the static variant, so those bundles import the
packages as before.

(cherry picked from commit b7ee831)

* perf(sdk): load the SSE transport with the first SSE session (#937)

A stdio server initializes the scope's TransportService, and with it
TransportSSEAdapter, which defined RecreateableSSEServerTransport at startup.
Its base class reads POST bodies with raw-body and content-type, so a stdio
server loaded raw-body, content-type, iconv-lite, http-errors and their
dependencies (16 files) that it never uses.

TransportSSEAdapter now imports the transport as a type and loads it with
loadSseTransport(), which TransportService awaits where it creates the
LocalTransporter of an SSE session, as it does for the Streamable HTTP
transport since #924.

(cherry picked from commit f13b8af)
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.

2 participants