Исходные данные: компиляция NodeJS проекта съедает почти 2Гб памяти. На рабочем компьюторе это меня не беспокоило, но на ноутбуке периодически возникал неприятный OutOfMemory.

Я начал исследовать, зачем довольно небольшой проект так много жрет? Довольно быстро в гугле я нашел неизвестную мне ранее опцию компилятора tsc --listFiles, которая выводит список используемых файлов: их оказалось 4500! Слишком много. Беглый просмотр списка показал, что в основном используются файлы из библиотек: googleapis, @hubspot, @redis-client. Я сохранил список в файл npx tsc --listFiles > .files.ls и начал измерения:

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

Библиотека предоставляет методы к апи этого сервиса. Методов очень много, а используем из них всего 6.

Создаю файл hubspot.js с одной строчкой: export {Client} from "@hubspot/api-client"; и переписываю импорты на этот файл. Отлично, я избавился от полторы тысячи файлов! Но без типизации можно сделать много ошибок. Поэтому добавляю рядом файл hubspot.d.ts, в котором прописываю типы только для нужных апи:

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;
        };
    }
}

Проверяем: npx tsc --listFiles | grep hubspot | wc -l 112. Good enough.

Google

Гугл также предоставляет апи к своим сервисам, которых, мне кажется, сильно больше, чем у Hubsot. Но у Google библиотека более продуманная, и позволяет импортировать каждый сервис отдельно:

Вместо

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

Нужно писать

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

Вроде бы известный способ уменьшения размера бандла, но на бекенде этим чаще всего не заморачиваются, а на фронтенде сборщик может выкинуть из бандла неиспользуемые файлы.

Проверяем: npx tsc --listFiles | grep google | wc -l 288.

Redis

Почти 500 файлов, ничего себе! Наверное я много чего не знаю про невероятные возможности этой БД. Стал изучать типизацию в @redis/client и быстро запутался, настолько она изощренная. Можно было бы взять другую библиотеку, но неизвестно, какие подводные камни она принесет. Вместо этого я просто скопировал типизацию используемых методов и немного упростил ее, убрав возможность использовать Buffer вместо 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;

Осталось только два файла из 491.

Итого

Количество используемых файлов сократилось с 4576 до 1397, потребление памяти упало в два раза до 1Гб, время компиляции на ноутбуке сократилось значительно. Изменения затронули всего 24 файла в проекте, так что code review будет простым.

Некоторые библиотеки предоставляют возможность импортировать отдельные свои части - этим стоит пользоваться, даже если размер бандла вам не важен. Для других библиотек можно использовать отдельные типы для ускорения компиляции. К сожалению, все еще существуют популярные библиотеки, в которых типизация настолько плохая, что лучше бы ее не было. При выборе библиотеки стоит обращать на это внимание.

Главным плюсом я вижу возможность не беспокоиться об OutOfMemory на ноутбуке, ну и пусть все машинки делают чуть меньше работы, надеюсь за это они меня пощядят при восстании.

Комментарии (21)