Ask
118
@orm_tamsin ·

tsc takes 96s on a 1,400 file pnpm workspace and tsserver dies in vscode tsconfig

Six packages in a pnpm workspace, roughly 1,400 files, no project references yet — everything resolves through path aliases into source. skipLibCheck is already on. Full tsc --noEmit is 96 seconds and tsserver climbs to about 4 GB and restarts itself every twenty minutes, which is worse than the build time.

I would rather find out where the time goes than start randomly deleting types. What is the order of operations here?

9 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @seedstart_sim · 2mo ago · 3 replies

    Measure, then fix, in that order, because the answer is almost never where people expect.

    tsc -p . --noEmit --generateTrace ./trace --incremental false
    

    Open trace/trace.json in a trace viewer and sort by duration. What you are looking for is a single wide checkSourceFile bar, and under it usually one absurd structuredTypeRelatedTo. Nine times in ten it is one of: a generated client (Prisma, GraphQL codegen, an OpenAPI SDK) that ships a 40,000 line .d.ts, a schema chain producing a monstrous inferred type, or a homegrown deep-partial/path helper.

    Once you know: project references with composite: true and tsc -b so untouched packages are skipped entirely, and explicit return type annotations on everything exported from a package. That second one is unglamorous and it is the biggest lever I know — every unannotated exported function makes every consumer re-derive the return type from the body.

    79
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @deploy_friday_ok · 2mo ago

      Explicit return types on the public surface has been worth 30-40% in every repo I have done it to, and it is mechanical enough that you can do it package by package on a slow afternoon. It also makes the diffs on your API surface visible in review, which turns out to be the bigger win.

      17
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @orm_tamsin · 2mo ago

      Trace found it in about four minutes. One generated API types file, 11 seconds on its own, and a second one at 6. Both are codegen output that nothing needed to deeply check.

      26
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @first_repo_finn · 2mo ago · 2 replies

    Treat the editor problem and the CI problem as two problems. They usually share a cause but not always, and 4 GB of tsserver is its own thing.

    Open the TS server log from the command palette and look at what it is loading. The classic is an include that pulls dist back in, so you type-check your own build output alongside your source and double everything. Second classic is one enormous inferred type that the editor re-derives on every keystroke.

    "typescript.tsserver.maxTsServerMemory": 8192 buys you a working day. It is a painkiller, not a fix.

    44
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @visa_run_val · 2mo ago

      Checking whether dist is in include should be step zero for anyone reading this later. It costs ten seconds to check and I have found it in three separate repos that all thought they had a mysterious performance problem.

      15
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @wellhead_wanda · 2mo ago

    With skipLibCheck on, the remaining declaration cost is coming from your own packages. If any of them emit .d.ts full of inline anonymous types rather than named ones, every consumer re-instantiates that structure from scratch.

    isolatedDeclarations (5.5 and later) forces you to annotate anything exported, which is annoying for a week and then makes declaration emit close to free. Worth turning on for the shared packages even if you never turn it on for apps.

    33
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @resole_ruth · 2mo ago

    For what it is worth: moving to tsc -b with references took our full check from about 90 seconds to 12 on the incremental path. The migration is a day of converting path aliases into references and updating imports, and it is dull work with no cleverness in it, but the payoff is immediate and permanent.

    21
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @mise_en_mess · 2mo ago · 2 replies

    Separate thought: take type checking out of the dev loop entirely. Let Vite strip types, run tsc --noEmit --watch in a second terminal, and gate CI on it. Waiting on the type checker while you edit is a choice, not a requirement.

    9
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @first_repo_finn · 2mo ago

      Works right up until someone stops looking at the second terminal for two days. Fine with a status bar item or a pre-push hook, risky as a pure convention.

      7
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report