One of the ways to speed up TypeScript compilation

One of the ways to speed up TypeScript compilation
6 min read

Source data: Compiling a NodeJS project consumes almost 2GB of memory. It didn't bother me on my work computer, but on my laptop, I periodically encountered an unpleasant OutOfMemory error.

I began to investigate why such a small project was consuming so much memory. Quite quickly, I found an unknown to me before compiler option sc --listFiles on Google, which outputs the list of used files: there were 4500 of them! Too many. A quick look at the list showed that the mostly used files were from libraries: googleapis, @hubspot, @redis-client. I saved the list to a file with the command npx tsc --listFiles > .files.ls and began measuring:

cat .files.ls | grep redis | wc -l     491
cat .files.ls | grep google | wc -l   907
cat .files.ls | grep hubspot | wc -l   1682

Hubspot

The library provides methods to access the API of this service. There are many methods, but we only use 6 of them. I create a file hubspot.js with one line: export {Client} from "@hubspot/api-client"; and rewrite the imports to this file. Great, I got rid of one and a half thousand files! But without typing, you can make many mistakes. Therefore, I add a file hubspot.d.ts next to it, in which I specify types only for the necessary APIs.

import {IHttpOptions} from "@hubspot/api-client/lib/src/services/http/IHttpOptions";
import IConfiguration from "@hubspot/api-client/lib/src/configuration/IConfiguration";
import {PromisePipelinesApi} from "@hubspot/api-client/lib/codegen/crm/pipelines/types/PromiseAPI";
import {PromiseCoreApi} from "@hubspot/api-client/lib/codegen/crm/properties/types/PromiseAPI";
import {PromiseSearchApi} from "@hubspot/api-client/lib/codegen/crm/contacts/types/PromiseAPI";

export declare class Client {
    constructor(config?: IConfiguration);
    apiRequest(opts?: IHttpOptions): Promise<import("node-fetch").Response>;
    crm: {
        deals: {
            searchApi: PromiseSearchApi
        };
        contacts: {
            searchApi: PromiseSearchApi
        };
        properties: {
            coreApi: PromiseCoreApi;
        };
        pipelines: {
            pipelinesApi: PromisePipelinesApi;
        };
    }
}

Checking:

We check with the command npx tsc --listFiles | grep hubspot | wc -l and get the result of 112. Good enough.

Google

Google also provides APIs to their services, which, I think, are much more than those of Hubspot. But Google's library is more thought-out and allows importing each service separately:

Instead of:

import { google } from 'googleapis';
google.cloudresourcemanager('v1')...

We need to write:

import { cloudresourcemanager } from 'googleapis/build/src/apis/cloudresourcemanager'
cloudresourcemanager('v1')...

Bundle size reduction

This is a well-known way to reduce bundle size, but on the backend, people usually don't bother with it, while on the frontend, a bundler can remove unused files from the bundle. Checking: npx tsc --listFiles | grep google | wc -l gives us 288.

Redis

Almost 500 files, wow! I probably don't know a lot about the incredible capabilities of this database. I started to study typing in @redis/client and quickly got confused because it's so sophisticated. I could take another library, but I don't know what pitfalls it will bring. Instead, I just copied the typing of the used methods and simplified it a bit, removing the ability to use Buffer instead of string:

export type RedisClientType = {
    on(type: 'error', cb: (err: Error) => void | any);
    on(type: 'end', cb: (err: any) => void | any);
    connect(): Promise<void>;
    set(key: string, value: string | number, options?: SetOptions): Promise<boolean>;
    del(keys: string | Array<string>): Promise<void>;
    get(key: string): Promise<string>;
    publish(channel: string, message: string): Promise<void>;
    subscribe: (channels: string | Array<string>, listener:  (message: string) => unknown) => Promise<void>;
}

export declare function createClient(config: {
    url: string;
    password: string;
}): RedisClientType;


declare type MaximumOneOf<T, K extends keyof T = keyof T> = K extends keyof T ? {
    [P in K]?: T[K];
} & Partial<Record<Exclude<keyof T, K>, never>> : never;
declare type SetTTL = MaximumOneOf<{
    EX: number;
    PX: number;
    EXAT: number;
    PXAT: number;
    KEEPTTL: true;
}>;
declare type SetGuards = MaximumOneOf<{
    NX: true;
    XX: true;
}>;
interface SetCommonOptions {
    GET?: true;
}
export declare type SetOptions = SetTTL & SetGuards & SetCommonOptions;

Conclusion

The number of used files has been reduced from 4576 to 1397, memory consumption has halved to 1GB, and compilation time on the laptop has been significantly reduced. The changes affected only 24 files in the project, so the code review will be simple.

Some libraries allow you to import separate parts - use this even if bundle size is not important to you. For other libraries, separate types can be used to speed up compilation. Unfortunately, there are still popular libraries where typing is so bad that it's better not to have it. When choosing a library, it's worth paying attention to this.

The main advantage I see is the ability not to worry about OutOfMemory on the laptop, and let all machines do a little less work, I hope they will spare me for this during the uprising.

In case you have found a mistake in the text, please send a message to the author by selecting the mistake and pressing Ctrl-Enter.
Jacob Enderson 4.1K
I'm a tech enthusiast and writer, passionate about everything from cutting-edge hardware to innovative software and the latest gadgets. Join me as I explore the...
Comments (0)

    No comments yet

You must be logged in to comment.

Sign In / Sign Up