TypeScript Performance: Going Beyond The Surface
· at TypeScript Congress · Online 🌍
Abstract
Do you ever find yourself pondering how to identify and address performance issues in TypeScript to maximize the effectiveness of your code? If so, join us for a talk on the performance of TypeScript and the techniques you can use to get the most out of your code. We’ll delve into various ways to debug performance, explore how to leverage the power of the TypeScript compiler to detect potential performance issues and use the profiling tools available to track down the underlying bottlenecks.
Talk Notes
What compiler compiles
TypeScript program
An object that has all the compilation context.
Scanner
Scans the source code and convert it into a list of tokens.
Parser
Brings context to the scanner. For example: it sees a FunctionKeyword
so it
knows it’s gonna be a FunctionDeclaration
and then if there’s an Identifier,
it’s gonna be this function Identifier, its name, and then we’re gonna have
parameters, and so on.
In reality, there’s some back and forth communication between parser and lexer. Parser is responsible for creating a correct AST, but it asks scanner to do some extra reading. Parser controls the scanner.
Binder
It results in errors about the whole context.
Main responsibilities:
- Creates a symbol table — additional metadata for each node. It will be used for later phases. Has information on where identifiers are defined — scopes. Keeps track of which scope you’re in (makes go to definition in IDE work).
- Sets up parent on syntax noes. Later checker can go up if needed and investigate the nodes above to get proper type.
- Makes flow nodes — TS needs to keep track of scopes, what types occur where and where possible mutations occur.
- Validates script vs module conformance.
It’s a single run through the entire tree.
Checker
Includes most of the diagnostics. It’s huge. For everything in AST there are
checking functions like checkVariableStatement
,
checkGrammarVariableDeclarationList
, isTypeRelatedTo
.
Two major responsibilities:
- It checks if things are assignable to other things.
- Type inference — if there are “gaps”, it tries to fill them — this is why we store so much information with a binder.
Transformer
It takes the AST that we have and to get JavaScript code, it strips all the types and optionally applies some transformers to e.g. support modern syntax.
When creating an AST, TS keeps track of all the transformers that’s gonna be needed. E.g. if it sees an ES2018 token and the target it older, it will know that an ES2018 transformer will be needed.
And to get declaration files, it strips the code bit. DTS Transformer often asks the type checker about the types. Especially when some variables are not annotated.
Emitter
We are getting the files that we requested.
Debugging
—diagnostics/—extendedDiagnostics
It’s quite useful to see what’s going on — what compiler steps are taking significant amount of time.
The three most expensive steps are usually parsing, binding, and a checker.
—listFiles
$ tsc --noEmit --listFiles | xargs stat -f "%z %N" | npx webtreemap-cli
$ tsc --noEmit --listFiles | xargs stat -f "%z %N" | npx webtreemap-cli
—showConfig
---generateTrace
$ tsc --generateTrace outDir
$ tsc --generateTrace outDir
Tool to analyze trace:
$ npx @typescript/analyze-trace ./outDir
$ npx @typescript/analyze-trace ./outDir
Improving
1. Check tsconfig — especially include/exclude settings
2. Name complex types
Examples:
3. Make your types/code simpler
4. Help TypeScript skip inference (if you really need to)
- GraphQL Code Generator example
5. Be reasonable
So, my favourite thing — a traceResolution flag. It makes it easier to identify the parts of a program that are taking the most time to compile. It can tell you which of your files to examine more closely.
- String literal templates example
Use —incremental flag
Still bad? Open an issue.
Example of a nice issue: https://github.com/microsoft/TypeScript/issues/53761