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.
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.
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])));
{"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.
export {};
type Label = { text: string };
function makeLabel(value: number): Label { return { text: `item-${value}` }; }
const label = makeLabel(3);
console.log(label.text);
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.