Course outline

TypeScript / Advanced / Lesson 16

Build performance and project boundaries

Measure checker and emission costs independently, reduce accidental program scope, and use project boundaries without claiming universal speedups.

About 5 min readContent reviewed
On this page

Lesson goals

  • Collect reproducible compiler performance evidence.
  • Explain incremental builds, declaration boundaries, and references.
  • Keep emitted runtime behavior unchanged while simplifying types.

Identify which stage is slow

A slow development loop can come from file discovery, parsing, module resolution, type checking, declaration generation, JavaScript emission, bundling, or runtime startup. Measure the stage before prescribing a flag. TypeScript extendedDiagnostics can report aggregate compiler work for a specific configuration; tracing tools provide more detail but require careful interpretation and can include project paths. Record compiler version, file count, cold or warm state, and command. Compare equivalent workloads and report variability. A one-time local duration is not a universal promise that an interface, alias, or build mode is always faster.

Name a stable result rather than exposing implementation expansion

The first function returns a small named interface. This gives consumers a readable boundary and prevents incidental local changes from producing an unnecessarily elaborate exported inferred type. It is not a benchmark proving interfaces always outperform aliases; actual checker cost depends on the model and workload. The complete example prints a correctness result so a performance refactor can be compared against it. Explicit public return types can also make declaration review easier. Avoid changing runtime arithmetic or mutation while claiming to perform only a type-level simplification. Measure both the build and the unchanged behavioral checks.

typescript A compact named output contract
export {};
interface Summary { count: number; total: number }
function summarize(values: readonly number[]): Summary {
  return { count: values.length, total: values.reduce((sum, value) => sum + value, 0) };
}
console.log(JSON.stringify(summarize([2, 4, 6])));
Expected output
{"count":3,"total":12}

Three small values total twelve. The declared interface does not change the runtime calculation.

Check only the files and ambient types the project needs

Unexpected include patterns, imported generated output, duplicate dependency trees, or unnecessary global type packages can expand a program. Inspect the actual file and resolution lists before excluding files blindly; an imported file can enter the program despite a discovery exclusion. types controls selected ambient packages and is not a general rule that prevents every imported declaration from being checked. Keep output outside source discovery and separate browser and server contexts where their globals differ. The second complete module demonstrates a tiny self-contained dependency boundary with no ambient application globals; real improvements require measuring the actual project graph.

typescript A small explicit module contract
export {};
type Label = { text: string };
function makeLabel(value: number): Label { return { text: `item-${value}` }; }
const label = makeLabel(3);
console.log(label.text);
Expected output
item-3

The function creates one ordinary object. Its type alias does not add runtime work or remove external dependencies from a larger project.

Reuse work through documented project boundaries

incremental can persist compiler build information to avoid repeating suitable work, while project references can split a codebase into related composite projects. Build mode coordinates referenced projects and their outputs according to their configuration. Benefits depend on dependency shape, change patterns, declaration boundaries, and tooling integration. A changed public type may invalidate many consumers even when one implementation edit would not. Keep build-info files and generated declarations consistent with the selected compiler and clean-build policy. Do not describe warm incremental performance as cold full-build performance, and verify that a clean build still succeeds from the declared sources.

Preserve diagnostics while reducing cost

skipLibCheck can reduce declaration checking but can also hide inconsistencies and does not repair invalid types. isolatedModules checks constraints useful for single-file transformers; it is not equivalent to full semantic checking. In the course baseline, tools that strip types must still be paired with the intended type-check step. Reduce repeated expensive type expressions or overly broad public models before weakening useful checks. A performance report should include a minimal safe reproducer, configuration, compiler version, relevant metrics, and unchanged runtime tests. Avoid uploading private source or traces to public reports without authorization; synthetic fixtures can demonstrate the issue clearly.

Common errors and fixes

A warm rebuild is advertised as full-build performance.

Why it happens
Cache state and workload were omitted.
How to fix it
Measure and label cold and incremental cases separately.
Prevent it next time
Record compiler, configuration, and cache assumptions.

Generated output expands the source program.

Why it happens
Output paths overlap discovery or are imported accidentally.
How to fix it
Separate output and inspect the actual program file graph.
Prevent it next time
Keep clean-build verification in the workflow.

skipLibCheck is treated as a declaration repair.

Why it happens
Skipping evidence was confused with correcting a contract.
How to fix it
Resolve the incompatible declarations and review the tradeoff.
Prevent it next time
Track why any check is intentionally skipped.

Check your understanding

1. A compiler upgrade appears slower only on the first run. Design a fair comparison before changing project options.

Show worked solution

Pin both compiler versions and run the same source, options, dependency tree, and machine conditions. Compare cold builds separately from repeated warm builds, recording build-info and filesystem-cache assumptions. Gather diagnostics for the stage that changed. A first-run difference does not justify disabling strict checks without evidence of the underlying cost.

2. An expensive exported conditional type is simplified to Summary. What tests should show that the change remains acceptable for consumers?

Show worked solution

Compile representative consumer calls and verify the properties and relationships they rely on still exist. Compare generated declarations and test intentional rejection cases. Run the same runtime report tests to ensure no implementation behavior changed. Measure checker cost with the fixed compiler and workload, and document any loss of type precision rather than silently calling it a compatible optimization.

Key takeaways

  • Measure stages and cache states separately.
  • Use project boundaries that match real dependencies.
  • Do not confuse skipping checks with fixing types.

Official references