Profiling TypeScript Code: Finding Bottlenecks in Practice
\nEvery developer has encountered a situation where an application starts to slow down, and the cause is not obvious. In the world of TypeScript, where code is transpiled to JavaScript, bottlenecks can occur both at runtime and due to the specifics of the transpilation itself. In this article, we will explore practical methods for profiling TypeScript code that will help identify problematic areas and improve your application's performance.
\n\nWhy Profiling TypeScript is Important
\nTypeScript adds static typing, but at runtime, regular JavaScript is executed. This means bottlenecks can be related not only to algorithms but also to how TypeScript is compiled. For example, excessive use of decorators or complex generics can lead to suboptimal code generation. Profiling helps answer the questions:
\n- \n
- Which functions take the longest to execute? \n
- How much memory does the application consume? \n
- Where do frequent garbage collections occur? \n
- Which code sections block the main thread? \n
Without profiling, you risk optimizing the wrong sections, wasting time on micro-optimizations that won't yield noticeable improvements.
\n\nTools for Profiling TypeScript
\nFor profiling TypeScript code, we will use three main tools that cover most scenarios: from browser applications to server-side solutions on Node.js.
\n\n1. Chrome DevTools for Browser Applications
\nThe most accessible and powerful tool. Open the Performance tab in Chrome DevTools, start recording, and perform the desired action in the application. After stopping, you will get a flame chart — a visual representation of function calls. Here's how it looks in practice:
\n// Example of slow TypeScript code\nfunction processData(data: number[]): number[] {\n const result: number[] = [];\n for (let i = 0; i < data.length; i++) {\n // Potential bottleneck: frequent calls\n result.push(data[i] * Math.sqrt(data[i]));\n }\n return result;\n}\n\n// Usage\nconst largeArray = Array.from({ length: 100000 }, (_, i) => i);\nconsole.time('processData');\nprocessData(largeArray);\nconsole.timeEnd('processData'); // ~15ms\n\nIn Chrome DevTools, you will see that the processData function takes significant time, and inside it — many calls to Math.sqrt. This is a hint that you can replace the iteration with map or vectorize the calculations.
2. Node.js Profiler for Server Applications
\nNode.js has a built-in profiler. Run the application with the --prof flag:
node --prof dist/app.js\n\nAfter completion, a file isolate-*.log will be generated. Convert it to a readable format:
node --prof-process isolate-*.log > processed.txt\n\nAn example of a typical problem — frequent allocations in loops:
\n// Bad example: creating objects in a loop\ninterface User {\n id: number;\n name: string;\n}\n\nfunction createUsers(ids: number[]): User[] {\n const users: User[] = [];\n for (const id of ids) {\n users.push({ id, name: `User_${id}` }); // New allocation\n }\n return users;\n}\n\nThe profiler will show that the createUsers function creates many temporary objects, increasing the load on the garbage collector. Optimization: pre-allocate memory or use an object pool.
3. Benchmark.js for Microbenchmarks
\nWhen you need to compare the performance of two implementations, use the benchmark.js library. Install it via npm:
npm install benchmark @types/benchmark\n\nExample of comparing two ways to filter an array:
\nimport Benc