Skip to content

perf: load TypeScript through a single require path - #3255

Draft
JoshuaKGoldberg wants to merge 1 commit into
mainfrom
perf/startup-module-loading
Draft

perf: load TypeScript through a single require path#3255
JoshuaKGoldberg wants to merge 1 commit into
mainfrom
perf/startup-module-loading

Conversation

@JoshuaKGoldberg

Copy link
Copy Markdown
Collaborator

SASSAFRAS

PR Checklist

Overview

typescript is CommonJS, so importing it from ESM makes Node run cjs-module-lexer across all ~9 MB of typescript.js to enumerate the named exports for an ESM namespace — work enableCompileCache() does not cover. packages/ts-patch/src/install-patch.ts already loads TypeScript with require, so the same library was being loaded through both a CJS and an ESM path in one process.

This adds two tiny shims to @flint.fyi/typescript-language and points internal value imports at them:

  • @flint.fyi/typescript-language/typescriptcreateRequire(import.meta.url)("typescript"), re-exported as a default plus the enums and helpers used across the repo.
  • @flint.fyi/typescript-language/ts-api-utilscreateRequire(import.meta.url)("ts-api-utils"), which picks up that package's CJS build.

The second shim is what makes the first one pay off. ts-api-utils's ESM build does import ts from "typescript", so as long as anything in the graph reaches it, the lexer pass happens anyway; its CJS build requires TypeScript and reuses the instance already in the require cache. With both in place, cjs-module-lexer drops from ~68 ms to ~3 ms of a two-file run's CPU profile, and typescript.js is loaded exactly once, by install-patch.

The convention after this change is: types come from "typescript" directly (import type is erased, so it costs nothing), values come from the shim. In the 70 files that used import ts from "typescript" for both, ts now names the type-only namespace and typescript names the runtime module.

Two supporting changes:

  • packages/ts-patch/src/install-patch-hooks.ts only patched TypeScript's source when load handed it a Buffer. registerHooks also intercepts require(), where Node hands it a string — so under Vitest the patched build was silently skipped once TypeScript came in through require. It now handles both, which is what keeps .astro/.vue extension support (_flintGetExtraSupportedExtensions) working.
  • ts-api-utils is no longer referenced from packages/performance, so its dependency entry is removed (with the matching pnpm-lock.yaml importer entry). This is the one change I could not verify with an install — my local pnpm is v10 and this repo needs v11, so the lockfile edit is by hand: the three-line importer entry for packages/performance is dropped and nothing else. Worth a pnpm install --lockfile-only before merging.

Measurements

node packages/flint/bin/index.js --cache-ignore --skip-formatting --skip-language-reports, hyperfine, 3 warmups × 10 runs, alternating branches within a single session (the machine drifts by 10–20% over tens of minutes, so back-to-back single runs are not comparable):

files rules main this branch delta
2 1 826 ms ± 28 ms 656 ms ± 33 ms −170 ms (−21%)
2 many 841 ms ± 30 ms 673 ms ± 34 ms −168 ms (−20%)
256 many 1.368 s ± 62 ms 1.194 s ± 166 ms −174 ms (−13%)
1024 many 3.043 s ± 70 ms 2.819 s ± 113 ms −224 ms (−7%)

Startup only, no linting — loading install-patch plus the @flint.fyi/ts plugin graph and exiting:

main this branch delta
module graph load 435 ms ± 37 ms / 449 ms ± 21 ms 280 ms ± 22 ms / 284 ms ± 19 ms −158 ms (−36%)

The --version benchmark from the issue investigation is flat, and deliberately reported as such:

hyperfine -N --warmup 3 'node packages/flint/bin/index.js --version'
main:        139–140 ms
this branch: 143–145 ms

--version short-circuits before any plugin is imported, so it never reaches a value import of typescript — it measures install-patch and the CLI shell only. It is not a useful isolation for this change; the module-graph row above is.

Not covered

  • Bundled plugin output (the other half of ⚡Performance: ~280ms of CLI startup spent loading modules that bundling and require() avoid #3250) is deferred. Worth noting that the published packages already get most of that win: @flint.fyi/ts's publishConfig points at dist/index.mjs, a single ~918 KB bundle, so an installed Flint does not load 301 separate rule modules. The 425 ms → 224 ms gap in the issue is specific to running from source, which is what packages/flint/bin/index.js does and what these benchmarks measure. Closing it would mean making the CLI prefer dist over src, i.e. making a fresh clone require a build before flint runs — a build-system change rather than a perf change, so I left it alone.
  • typescript/lib/tsserverlibrary is a non-issue on TypeScript 6. @typescript-eslint/project-service still requires it, but in 6.0.3 that file is 1 KB and its whole body is module.exports = require("./typescript.js"), so it costs nothing and does not produce a second copy of TypeScript.
  • The four e2e test failures on this branch (cspell, directives, no-reports, typescript) reproduce identically on main at 116a836 and are unrelated. The packages/site lint errors (missing astro:content types) likewise reproduce on main.

Routes every internal value import of `typescript` and `ts-api-utils`
through `createRequire` shims in `@flint.fyi/typescript-language`, so
Node no longer runs cjs-module-lexer across the whole TypeScript bundle
to build an ESM namespace for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: da723b4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@netlify

netlify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy Preview for flint-fyi ready!

Name Link
🔨 Latest commit da723b4
🔍 Latest deploy log https://app.netlify.com/projects/flint-fyi/deploys/6a85eb70dd0eba00086f6adb
😎 Deploy Preview https://deploy-preview-3255--flint-fyi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@lishaduck

Copy link
Copy Markdown
Member
  • my local pnpm is v10 and this repo needs v11

And it automatically switches? Is this running in a sandboxed cloud environment or is it just stingy?

@lishaduck

lishaduck commented Aug 19, 2026

Copy link
Copy Markdown
Member

And personally I still think it's better to do this in the build process

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.

⚡Performance: ~280ms of CLI startup spent loading modules that bundling and require() avoid

2 participants