Один из способов ускорения компиляции TypeScript

Исходные данные: компиляция 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 на ноутбуке, ну и пусть все машинки делают чуть меньше работы, надеюсь за это они меня пощядят при восстании.

@fransua
04.05.2023 20:44 UTC
Первоисточник

Комментарии

@sanex3339
04.05.2023 17:24 UTC
+1

А skipLibCheck не помогал?

@fransua
04.05.2023 18:07 UTC
0

Нет, он включен, но все равно файлы используются. Попробовал выключить, количество файлов увеличилось на целых 6, но появились ошибки.

@serginho
04.05.2023 18:57 UTC
+1

Посмотрите esbuild. Написан на Rust, мой проект билдит за секунду, в то время как tsc секунд 40.

@
04.05.2023 21:26 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
05.05.2023 04:10 UTC
0

да

@fransua
04.05.2023 21:43 UTC
0

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

@WalkInWay
04.05.2023 21:56 UTC
0

Swc написан на Rust. А esbuild написан на Go (https://esbuild.github.io/faq/#why-is-esbuild-fast)

@amakhrov
04.05.2023 21:30 UTC
0

Потребление памяти-то вы снизили. Но теперь ответственность за правильные типы библиотек - на вас. Изменится зависимость, а вы не поправите свою обертку с типами - что-нибудь упадет.

Кстати, такой способ импорта не очень совместим с ES Modules. Если в зависимости прописаны точки входа exports в package.json , то импортировать можно только из этих точек входа. https://nodejs.org/api/packages.html#package-entry-points

@fransua
04.05.2023 21:55 UTC
0

У нас зафиксированы версии в package.json, обновляем редко и при большой нужде, даже changelog смотрим.

Про es modules Вы правы, там еще импорт директории запрещен, нужно указывать index.js. Но для этого хотя бы можно написать plugin, а с прописанными exports я даже не знаю, что и делать.

05.05.2023 09:05 UTC
+1

Moжно делать для package собственные патчи при помощи yarn patch/pnpm patch

https://yarnpkg.com/cli/patch/

https://dimava.github.io/pnpm/cli/patch/

https://github.com/antfu/pnpm-patch-i

@kolya7k
05.05.2023 09:05 UTC
-3

Ох и горе-программисты же пошли… Во всех книгах типа «10 способов выстрелить себе в ногу» пишут, что это один из способов.

Вы вообще знаете, что такое инкапсуляция и зачем она нужна? Я бы у себя на проектах за такое заставил бы перечитывать все эти книги. Вот оно, поколение JS, люди, которые, не работали ни с C/C++ ни с ассемблером…

Теперь вы не сможете обновить версии библиотек, даже если там найдутся критические уязвимости/ошибки или они перестанут поддерживаться.

Тому, кто будет поддерживать этот код после вас прийдётся туго… Даже разобраться в том, что где и откуда уже непросто, а зачем это было сделано вообще и подавно.

Вы залезли во внутренние типы библиотек и если библиотека решит полностью поменять «внутрянку» с сохранением публичного интерфейса то у вас все сломается. Инкапсуляция как раз про то, чтобы таким образом не стрелять себе в ногу.

В общем, уровень начинающего джуна по моему опыту.

@fransua
05.05.2023 09:28 UTC
+3

Лет 7 назад я тоже верил в китов ООП, SOLID, и ругался, когда находил в БД ненормализованные таблички.

Инкапсуляция здесь нарушается, Вы правы. Но я не считаю её незыблемым правилом, которое нельзя нарушать ни при каких условиях. Есть минусы, которые Вы описали - будет сложнее переход на новую версию библиотеки при обратно несовместимом изменение апи. Есть плюсы.

Можно представить это как адаптер и инверсию зависимостей. Если hubspot изменит свое api, нужно будет поменять адаптер - hubspot.js, hubspot.d.ts и реализовать старые методы через новый апи, а не шерудить по всему проекту.

Конечно, нужно было бы просто выкинуть @redis/client и взять более подходящую библиотеку, уверен в npm есть что-то годное. Но, скорей всего, в проекте уже есть зависимости на определенные особенности этой библиотеки (абстракции протекают) и они сломаются при переходе на другую библиотеку. Может память немного протечь, может гонка начнется. Это может произойти и при переходе на новую версию, поэтому мы фиксируем версии зависимостей и переходим только если ооочень надо.
Добавлю в код комменты для других разрабов, возможно оно действительно неочевидно, зачем.

Переход на личности весьма неприятен, и не обоснован, я намног больше воспитан C#, чем js, с ассемблером, слава богу, не работал, а вот с C/C++ довелось, и не думаю, что это хороший язык для воспитания хорошего вкуса у программиста, слишком много на нем очень плохого кода.

05.05.2023 13:18 UTC
0

7 лет назад у меня уже был общий опыт программирования 22 года :)

ООП и SOLID это скорее «идеалы» к которым нужно стремиться, но которыми можно пожертвовать ради - ради чего? Производительности - только если итоговая выгода для ПОЛЬЗОВАТЕЛЕЙ продукта существенна и других способов сделать то же самое не нашлось, удобства - возможно, но очень осторожно, ведь что удобно одному неудобно другому.

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

Инкапсуляция же это не идеал, а «ограничитель» как раз для того, чтобы не делать так. Когда ей сознательно игнорируют это всё-равно что отцепить страховку скалолазам чтобы «по-быстрому» или удобнее кое-что сделать и потом обратно.

И я не переходил на личности, так как в моем комментарии нет упоминания или указания на конкретную личность. Есть оценка уровня, но это не является оскорблением или унижением.

Каждый раз когда вы чем-то жертвуете в проекте вы должны чётко понимать - ради чего? В данном посте автор приносит огромные жертвы ради… ускорения сборки проекта и решения проблем с нехваткой памяти. Что первое что второе можно решить заменив ПК и ноутбук на более производительные, либо использовать другие компиляторы типа того же esbuild. Неразумно.

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

Даже если у вас количество повторяющихся данных в кортежах 0.01%, то БД уже не будет соответствовать 1NF, поэтому нормализируют БД не с целью «соответствовать нормальным формам», а как раз с целью повысить эффективность, уменьшить потенциальные ошибки, упростить поддержку схемы и логики приложения.

05.05.2023 14:04 UTC
0

С точки зрения теории я с Вами согласен, все так и есть.
С одной стороны возрастающая сложность, с другой удобство пользователей. (У сервиса компиляции пользователи - программисты)
А на практике, в конкретном случае, вы действительно считаете, что это приведет к большим проблемам? Мне кажется, вероятность мала, а проблемы незначительны. Оценка, конечно, субъективна и зависит от проекта.

05.05.2023 20:20 UTC
0

Я не только считаю, я с этим сталкивался очень много раз. С позиции руководителя и лида особенно. Люди меняются и новички на проекте потом приходят ко мне с вопросами "А это зачем?". Я на 100% согласен про удобство ПОЛЬЗОВАТЕЛЕЙ, но тут достигается не удобство пользователей, а удобство одного конкретного человека со слабым ноутбуком :) Всё же все решения должны быть подчинены эффективности бизнеса, а не чего бы то ни было.

05.05.2023 21:25 UTC
0

Хорошо написанный комментарий решит проблему?
Ноутбук не слабый, i7 10 серии, 16Гб. Но криво написанные библиотеки (а в случае redis это именно что криво и слишком вычурно) иногда непомерно тратят ресурсы.
Бизнес это в том числе про скорость багфиксов и выкатки фич, в том числе про удовлетворенность процессом разработчиков.

05.05.2023 16:07 UTC
0

Использование адаптера для увеличения скорости = совершенно валидный паттерн, как и денормализация некоторых таблиц БД, когда мы жертвуем ради скорости необходимостью поддерживать ее консистентность (кстати, NF применяется к модели данных, а не к физическим БД, поэтому, когда мы говорим об эффективности, мы должны указывать метрики, которые мы решаем).

Программирование - не магия следования AbstractBuilderFactoryAdapter, а инженерная/архитектурная практика. Например, если проект собирается 10 минут (ангуляр проект какой-нибудь), а после - 5, вы можете сами себе посчитать импакт на T2L.

05.05.2023 21:05 UTC
+1

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

Более того, как уже было много раз сказано даже тут - есть более "правильные" способы достичь этой же цели.

P.S.: NF это, грубо говоря, набор принципов. И они, по сути, могут применяться к чему угодно, ко всему, что подходит к определению модель данных, это верно. Но в обсуждении конкретно тут и в принципе используется уже какая-то "материальная" сущность модели данных. Иначе это будет обсуждение абстрактных единорогов в вакууме :)

И вообще :) Согласно методологии SMART желательно всегда оперировать измеримыми (и остальные SART) метриками, это тоже верно как то, что земля не плоская :)

Программирование вообще не про "скорость работы алгоритмов" или "скорость сборки" или что либо ещё. Программирование - про достижение бизнес-целей. Сеньор от остальных отличается как раз тем, что думает не о том как что и как запрограммировать, а о том, как добиться того, что нужно бизнесу (и продукту). Иногда, решения оказываются даже вне рамок программирования. Например, часто в результате меняется диздок, тз, а не реализуется то, что написано там.

Так же, лидов от остальных отличает наличие собственной философии программирования, тогда начинают появляются вопросы "что я делаю?" и "зачем я это делаю?" (не вопросы экзистенциального характера, а конкретные по продукту и бизнесу).

Моя личная философия (вернее её часть) программирования - код должны быть способны поддерживать даже идиоты, согласно её все "паттерны", "теории" и нормальные формы применяются только для достижения этого. НО я ранжирую выгоду от того или иного решения, грубо говоря, применяю среднее геометрическое взвешенное всех решений, в котором стоимость поддержи имеет больший вес, чем остальные, но скорость, красота, следование паттернам тоже их (веса) имеют.

Умение программировать и проектировать архитектуру это не практические навыки. Они сродни написанию картины или созданию музыки. Это творчество, которое невозможно формализовать. Но, к сожалению, так только у (хотел написать настоящих, но это было бы унижением) опытных программистов...

На мой взгляд опыт программистов по уровню можно разделить на части:
1. Знание синтаксиса языка
2. Знание возможностей языка и стандартной библиотеки
3. Знание паттернов программирования
4. Использование различных библиотек вместо своего кода
5. Знание внутренней реализации популярных контейнеров и алгоритмов
6. Понимание бизнес-логики и бизнес-потребностей
7. Умение обучать других программистов
8. Наличие своей философии программирования

Написал это всё не очень точно, я сейчас пью пиво :)

@CapToYou
05.05.2023 18:40 UTC
+1

Опыт программирования 29 лет и сокрытие = инкапсуляция. Ну ну... Каким боком тут инкапсуляция? :/

05.05.2023 20:15 UTC
0

Инкапсуляция - сокрытие реализации если говорить простыми словами. А у вас какое понимание?

@flancer
06.05.2023 04:15 UTC
0

Самый простой способ ускорения компиляции TypeScript'а - сразу кодировать на JavaScript'е ;)