Как стать автором
Обновить
VK
Технологии, которые объединяют

Производительность TypeScript

Время на прочтение 15 мин
Количество просмотров 16K
Автор оригинала: Daniel Rosenwasser

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

1. Написание легкокомпилируемого кода


1.1. Предпочтение интерфейсам, а не пересечениям (intersection)


Чаще всего простой псевдоним для типа объекта действует так же, как интерфейс.

interface Foo { prop: string }

type Bar = { prop: string };

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

Интерфейсы создают единый flatten-тип объекта, выявляющий конфликты свойств, которые обычно важно разрешать! А пересечения просто рекурсивно объединяют свойства, и в некоторых случаях генерируют never. Также интерфейсы лучше отображаются, в то время как псевдонимы типов к пересечениям нельзя отобразить в части других пересечений. Взаимосвязи типов между интерфейсами кешируются, в отличие от типов пересечений. И последнее важное различие в том, что при проверке на соответствие целевому типу пересечения каждый компонент проверяется до того, как будет выполнена проверка на соответствие типу «effective»/«flattened».

Поэтому рекомендуется расширять типы с помощью interface/extends, а не создавать пересечения типов.

- type Foo = Bar & Baz & {
-     someProp: string;
- }
+ interface Foo extends Bar, Baz {
+     someProp: string;
+ }

1.2. Использование аннотирования типов


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

- import { otherFunc } from "other";
+ import { otherFunc, otherType } from "other";

- export function func() {
+ export function func(): otherType {
      return otherFunc();
  }

1.3. Предпочтение базовым типам, а не множественным


Множественные типы — прекрасный инструмент: они позволяют выражать диапазон возможных значений для типа.

interface WeekdaySchedule {
  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";
  wake: Time;
  startWork: Time;
  endWork: Time;
  sleep: Time;
}

interface WeekendSchedule {
  day: "Saturday" | "Sunday";
  wake: Time;
  familyMeal: Time;
  sleep: Time;
}

declare function printSchedule(schedule: WeekdaySchedule | WeekendSchedule);

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

interface Schedule {
  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";
  wake: Time;
  sleep: Time;
}

interface WeekdaySchedule extends Schedule {
  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";
  startWork: Time;
  endWork: Time;
}

interface WeekendSchedule extends Schedule {
  day: "Saturday" | "Sunday";
  familyMeal: Time;
}

declare function printSchedule(schedule: Schedule);

Более реалистичный пример: когда пытаешься смоделировать все встроенные типы элементов DOM. В этом случае предпочтительно создавать базовый тип HtmlElement с частыми элементами, который расширяется с помощью DivElement, ImgElement и т. д., а не создавать тяжёлое множество DivElement | /*...*/ | ImgElement | /*...*/.

2. Использование проектных ссылок


При создании любой крупной кодовой базы на TypeScript полезно организовать её в виде нескольких независимых проектов. Каждый из них имеет собственный tsconfig.json с зависимостями в других проектах. Это может помочь избежать загрузки слишком большого количества файлов при одной компиляции, а также облегчить сочетание разных схем кодовой базы.

Есть несколько очень простых способов разделения кодовой базы на проекты. Например, проект для клиента, проект для сервера и общий между ними проект.

             ------------
              |          |
              |  Shared  |
              ^----------^
             /            \
            /              \
------------                ------------
|          |                |          |
|  Client  |                |  Server  |
-----^------                ------^-----

Тесты тоже могут быть выделены в отдельный проект.

             ------------
              |          |
              |  Shared  |
              ^-----^----^
             /      |     \
            /       |      \
------------  ------------  ------------
|          |  |  Shared  |  |          |
|  Client  |  |  Tests   |  |  Server  |
-----^------  ------------  ------^-----
     |                            |
     |                            |
------------                ------------
|  Client  |                |  Server  |
|  Tests   |                |  Tests   |
------------                ------------

Часто спрашивают: «Насколько большим должен быть проект?» Это примерно как спросить: «Насколько большой должна быть функция?» или «Насколько большим должен быть класс?» Во многом зависит от опыта. Скажем, можно делить JS/TS-код с помощью папок, и если какие-то компоненты достаточно взаимосвязаны, чтобы класть их в одну папку, то можно считать их принадлежащими к одному проекту. Кроме того, избегайте больших или маленьких проектов. Если один из них больше всех остальных вместе взятых, то это плохой знак. Также лучше не создавать десятки однофайловых проектов, потому что это увеличивает накладные расходы.

Почитать о межпроектных ссылках можно здесь.

3. Конфигурирование tsconfig.json или jsconfig.json


Пользователи TypeScript и JavaScript всегда могут настроить свои компиляции с помощью файла tsconfig.json. Для настройки редактирования JavaScript также можно использовать файлы jsconfig.json.

3.1. Определение файлов


Всегда проверяйте, что в ваших файлах конфигурации не указано сразу слишком много файлов.

В tsconfig.json можно определять файлы проекта двумя способами:

  • списком files;
  • списками include и exclude;

Основное различие между ними в том, что files получает список путей к исходным файлам, а include/exclude используют шаблоны подстановки (globbing patterns) для определения соответствующих файлов.

Определяя files, мы позволяем TypeScript быстро загружать файлы напрямую. Это может быть громоздко, если в проекте много файлов и лишь несколько верхнеуровневых входных точек. Кроме того, легко позабыть добавить новые файлы в tsconfig.json, и тогда вы столкнётесь со странным поведением редактора.

include/exclude не требуют определять все эти файлы, однако система должна обнаружить их, пройдясь по добавленным директориям. И если их много, компиляция может замедлиться. К тому же иногда в компиляцию включают многочисленные ненужные .d.ts и тестовые файлы, что тоже может снизить скорость и повысить потребление памяти. Наконец, хотя в exclude есть подходящие значения по умолчанию, в некоторых конфигурациях наподобие монорепозиториев есть «тяжёлые» папки вроде node_modules, которые тоже будут добавлены при компиляции.

Лучше всего поступать так:

  • Определить только входные папки вашего проекта (например, исходный код из которых вы хотите добавлять при компиляции и анализе).
  • Не смешивать в одной папке исходные файлы из разных проектов.
  • Если вы храните тесты в одной папке с исходниками, присваивайте им такие имена, чтобы их легко можно было исключить.
  • Избегайте создания в исходных папках больших сборочных артефактов и папок с зависимостями вроде node_modules.

Примечание: без списка exclude папка node_modules будет исключена по умолчанию. И если список будет добавлен, важно явно указать в нём node_modules.

Вот пример tsconfig.json:

{
    "compilerOptions": {
        // ...
    },
    "include": ["src"],
    "exclude": ["**/node_modules", "**/.*/"],
}

3.2. Контроль за добавлением @types


По умолчанию TypeScript автоматически добавляет все найденные в папке node_modules пакеты @types, вне зависимости от того, импортировали вы их или нет. Это сделано для того, чтобы определённые функции «просто работали» при использовании Node.js, Jasmine, Mocha, Chai и т. д., так как эти инструменты/пакеты не импортируются, а загружаются в глобальное окружение.

Иногда эта логика может замедлять компиляцию и редактирование программы. И даже приводить к конфликтам объявлений в многочисленных глобальных пакетах, которые вызывают подобные ошибки:

Duplicate identifier 'IteratorResult'.
Duplicate identifier 'it'.
Duplicate identifier 'define'.
Duplicate identifier 'require'.

Если глобальные пакеты не нужны, то можно в опции «types» в tsconfig.json/jsconfig.json определить пустую папку:

// src/tsconfig.json
{
   "compilerOptions": {
       // ...

       // Don't automatically include anything.
       // Only include `@types` packages that we need to import.
       "types" : []
   },
   "files": ["foo.ts"]
}

Если же вам нужны глобальные пакеты, внесите их в поле types.

// tests/tsconfig.json
{
   "compilerOptions": {
       // ...

       // Only include `@types/node` and `@types/mocha`.
       "types" : ["node", "mocha"]
   },
   "files": ["foo.test.ts"]
}

3.3. Инкрементальное генерирование проекта


Флаг --incremental позволяет TypeScript сохранять состояние последней компиляции в файл .tsbuildinfo. Он используется для определения минимального набора файлов, которые могут быть перепроверены/перезаписаны с последнего запуска, по примеру режима --watch в TypeScript.

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

3.4. Пропуск проверки .d.ts


По умолчанию TypeScript полностью перепроверяет все .d.ts файлы в проекте, чтобы найти проблемы и несогласованности. Но обычно это не нужно. Чаще всего уже известно, что эти файлы работают: способы расширения типов уже проверены, а важные объявления всё равно будут проверены.

TypeScript позволяет с помощью флага skipDefaultLibCheck пропускать проверку типов в поставляемых .d.ts файлах (например, в lib.d.ts).

Также вы можете включить флаг skipLibCheck для пропуска проверки всех .d.ts файлов в компиляции.

Эти две опции часто скрывают ошибки конфигурирования и конфликты в .d.ts файлах, так что рекомендуется использовать их только для ускорения сборки.

3.5. Более быстрые вариативные проверки


Список собак или животных? Можно ли привести List<Dоg> к List<Animаls>? Простой способ найти ответ заключается в структурном сравнении типов, элемент за элементом. К сожалению, это решение может оказаться очень дорогим. Но если мы достаточно узнаем о List<Т>, то сможем свести проверки на возможность присваивания к определению — допустимо ли отнести Dog к Animal (то есть без проверки каждого элемента List<Т>). В частности, нам нужно узнать вариативность типа параметра T. Компилятор может извлечь всю пользу из оптимизации только при включённом флаге strictFunctionTypes (иначе будет использовать более медленную, но более снисходительную структурную проверку). Поэтому рекомендуется собирать с флагом --strictFunctionTypes (который по умолчанию включён под --strict).

4. Настройка других сборочных инструментов


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

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


4.1. Одновременная проверка типов


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

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

В качестве примера можно привести плагин fork-ts-checker-webpack-plugin для Webpack, или аналогичный awesome-typescript-loader.

4.2. Изолированное создание файлов


По умолчанию для создания файлов в TypeScript требуется семантическая информация, которая может оказаться нелокальной по отношению к файлу. Это нужно для понимания, как генерируются фичи вроде const enum и namespace. Но иногда генерирование замедляется из-за необходимости проверки других файлов, чтобы сгенерировать результат для произвольного файла.

Нам достаточно редко нужны фичи, которым требуется нелокальная информация. Обычные enum можно использовать вместо const enum, а модули — вместо namespace. Поэтому в TypeScript есть флаг isolatedModules для выдачи ошибок на фичах, которым требуется нелокальная информация. С этим флагом вы сможете безопасно применять инструменты, использующие API TypeScript вроде transpileModule или альтернативные компиляторы вроде Babel.

Этот код не будет корректно работать в runtime с изолированным преобразованием файлов, потому что должны быть инлайнены значения const enum. К счастью, isolatedModules заранее предупредит нас.

// ./src/fileA.ts

export declare const enum E {
    A = 0,
    B = 1,
}

// ./src/fileB.ts

import { E } from "./fileA";

console.log(E.A);
//          ~
// error: Cannot access ambient const enums when the '--isolatedModules' flag is provided.

Помните: isolatedModules не ускоряет автоматически генерирование кода. Он лишь предупреждает об использовании фичи, которая может не поддерживаться. Вам нужна изолированное генерирование модулей в разных сборочных инструментах и API.

Изолированно создавать файлы можно с помощью таких инструментов:


5. Расследование проблем


Есть разные способы разобраться в причинах, когда что-то идёт не так.

5.1. Отключение плагинов редактора


Плагины могут влиять на работу редактора. Попробуйте отключить их (особенно относящиеся к JavaScript/TypeScript) и посмотреть, улучшится ли производительность и отзывчивость.

Для некоторых редакторов есть свои рекомендации по повышению производительности, почитайте их. Например, у Visual Studio Code есть отдельная страница с советами.

5.2. extendedDiagnostics


Можно запустить TypeScript с --extendedDiagnostics, чтобы увидеть, на что тратится время работы компилятора:

Files:                         6
Lines:                     24906
Nodes:                    112200
Identifiers:               41097
Symbols:                   27972
Types:                      8298
Memory used:              77984K
Assignability cache size:  33123
Identity cache size:           2
Subtype cache size:            0
I/O Read time:             0.01s
Parse time:                0.44s
Program time:              0.45s
Bind time:                 0.21s
Check time:                1.07s
transformTime time:        0.01s
commentTime time:          0.00s
I/O Write time:            0.00s
printTime time:            0.01s
Emit time:                 0.01s
Total time:                1.75s

Обратите внимание, что Total time не будет суммой всех перечисленных временных затрат, поскольку часть из них пересекается, а часть вообще не измеряется.

Самая релевантная для большинства пользователей информация:

Поле Значение
Files
Количество файлов, входящих в программу (что это за файлы, можно посмотреть с помощью --listFiles).
I/O Read time
Время, потраченное на чтение из файловой системы. Сюда входит и чтение папок из include.
Parse time
Время, потраченное на сканирование и парсинг программы.
Program time
Общее время на чтение из файловой системы, сканирование и парсинг программы, а также прочие вычисления графа. Эти этапы комбинируются здесь, потому что они должны быть разрешены и загружены, как только будут добавлены через import и export.
Bind time
Время, потраченное на сборку разной семантической информации, локальной по отношению к конкретному файлу.
Check time
Время, потраченное на проверку типов в программе.
transformTime time
Время, потраченное на переписывание TypeScript AST (деревьев, представляющих исходные файлы) в виде форм, работающих в старых runtime-средах.
commentTime
Время, потраченное на вычисление комментариев в генерируемых файлах.
I/O Write time
Время, потраченное на запись и обновление файлов на диске.
printTime
Время, потраченное на вычисление строкового представления генерируемого файла и сохранение его на диск.

Что вам может понадобиться с учётом этих входных данных:

  • Соответствует ли примерно количество файлов/строк кода количеству файлов в проекте? Если нет, попробуйте использовать --listFiles.
  • Значения Program time или I/O Read time выглядят большими? Проверьте корректность настроек include/exclude

Похоже, с другими таймингами что-то не так? Можете заполнить отчёт о проблеме! Что вам поможет в диагностике:

  • Запуск с emitDeclarationOnly, если значение printTime высокое.
  • Инструкции по Отчётам о проблемах с производительностью компилятора


5.3. showConfig


Не всегда понятно, с какими настройками выполняется компилирование при запуске tsc, особенно учитывая, что tsconfig.jsons может расширять другие конфигурационные файлы. showConfig может пояснить, что будет вычислять tsc.

tsc --showConfig

# or to select a specific config file...

tsc --showConfig -p tsconfig.json

5.4. traceResolution


Запуск с traceResolution подскажет, почему файл был добавлен в компиляцию. Данные довольно обширные, так что можно сохранить результат в файл:

tsc --traceResolution > resolution.txt

Если вы нашли файл, которого быть не должно, то можете поправить список include/exclude в файле tsconfig.json, или скорректировать настройки вроде types, typeRoots или paths.

5.5. Запуск одного tsc


Часто пользователи сталкиваются с низкой производительностью сторонних сборочных инструментов вроде Gulp, Rollup, Webpack и др. Запуск tsc --extendedDiagnostics для поиска основных расхождений между TypeScript и сторонним инструментом может указать на ошибки внешних настроек или неэффективность работы.

О чём нужно себя спросить:

  • Сильно ли различается время сборки с помощью tsc и инструмента, интегрированного с TypeScript?
  • Если сторонний инструмент имеет средства диагностики, то различается ли решение у TypeScript и стороннего инструмента?
  • Есть ли у инструмента собственная конфигурация, которая может быть причиной низкой производительности?
  • Есть ли у инструмента конфигурация для его интеграции с TypeScript, которая может быть причиной низкой производительности (например, опции для ts-loader)?

5.6. Обновление зависимостей


Иногда на проверку типов в TypeScript могут повлиять вычислительно сложные файлы .d.ts. Редко, но так бывает. Обычно это решается с помощью обновления на новую версию TypeScript (эффективнее) или новую версию пакета @types (который мог обратить регрессию).

6. Частые проблемы


Столкнувшись с затруднениями, вам захочется узнать о решениях частых проблем. Если нижеописанное не поможет, можно сообщить о проблеме.

6.1. Неправильная настройка include и exclude


Как уже упоминалось, опции include/exclude могут применяться ошибочно.

Проблема Причина Исправление
node_modules был случайно добавлен из глубже вложенной папки. Не был настроен exclude "exclude": ["**/node_modules", "**/.*/"]
node_modules был случайно добавлен из глубже вложенной папки. "exclude": ["node_modules"]
"exclude": ["**/node_modules", "**/.*/"]
Случайно добавлены скрытые файлы с точкой (например, .git). "exclude": ["**/node_modules"]
"exclude": ["**/node_modules", "**/.*/"]
Добавлены неожиданные файлы. Не был настроен include "include": ["src"]

7. Заполнение отчётов о проблемах


Если ваш проект уже правильно и оптимально сконфигурирован, то можно заполнить отчёт о проблеме.

Хороший отчёт содержит доступное и легковоспроизводимое описание проблемы. То есть содержит кодовую базу из нескольких файлов, которую можно легко клонировать через Git. При этом нет необходимости во внешней интеграции со сборочными инструментами, их можно вызвать через tsc, либо использовать изолированный код, применяющий TypeScript API. Нельзя приоритизировать кодовые базы, которые требуют сложных вызовов и настроек.

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

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

7.1. Отчёт о проблемах с производительностью компилятора


Иногда проблемы с производительностью возникают и при сборке, и при редактировании. Тогда целесообразно сосредоточиться на компиляторе TypeScript.

Во-первых, используйте «ночную» версию TypeScript, чтобы убедиться, что вы не столкнулись с уже решённой проблемой:

npm install --save-dev typescript@next

# or

yarn add typescript@next --dev

Описание проблемы с производительностью должно содержать:

  • Установленную версию TypeScript (npx tsc -v или yarn tsc -v).
  • Версию Node, в которой запускался TypeScript (node -v).
  • Результат запуска с опцией extendedDiagnostics (tsc --extendedDiagnostics -p tsconfig.json).
  • В идеале, нужен сам проект, демонстрирующий возникшую проблему.
  • Журнал профилировщика компилятора (файлы isolate-*-*-*.log и *.cpuprofile).

Профилирование компилятора

Важно предоставить нам диагностическую трассировку, запустив Node.js v10+ с флагом --trace-ic и TypeScript с флагом --generateCpuProfile:

node --trace-ic ./node_modules/typescript/lib/tsc.js --generateCpuProfile profile.cpuprofile -p tsconfig.json

Здесь ./node_modules/typescript/lib/tsc.js можно заменить любым путём, по которому установлена ваша версия компилятора TypeScript. А вместо tsconfig.json может быть любой конфигурационный файл TypeScript. Вместо profile.cpuprofile — выходной файл на ваш выбор.

Будет сгенерировано два файла:

  • --trace-ic сохранит данные в файл вида isolate-*-*-*.log (например, isolate-00000176DB2DF130-17676-v8.log).
  • --generateCpuProfile сохранит данные в файл с наименованием по вашему выбору. В примере выше это profile.cpuprofile.

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

Но если у вас есть какие-либо сомнения относительно их выкладывания на Github, напишите нам, и сможете прислать информацию приватно.

7.2. Отчёт о проблемах с производительностью редактора


У низкой производительности при редактировании может быть много причин. И команда создателей TypeScript может повлиять только на производительность языкового сервиса JavaScript/TypeScript, а также на интеграцию между языковым сервисом и определёнными редакторами (например, Visual Studio, Visual Studio Code, Visual Studio for Mac и Sublime Text). Убедитесь, что в вашем редакторе выключены все сторонние плагины. Это позволит убедиться, что проблема связана с самим TypeScript.

Проблемы с производительностью редактора немного сложнее, но применимы те же идеи: идеально подходят клонируемые минимальные кодовые базы с воспроизводимой проблемой. И в некоторых случаях мы можем подписать NDA, чтобы изучить и изолировать проблемы.

Всегда приветствуется добавление данных из tsc --extendedDiagnostics, но ещё лучше, если будет трассировка TSServer.

Получение журналов TSServer в Visual Studio Code

  1. Откройте командную панель, затем:
  2. Задайте опцию «typescript.tsserver.log»: «verbose»,.
  3. Перезапустите VS Code и воспроизведите проблему.
  4. В VS Code выполните команду TypeScript: Open TS Server log.
  5. Должен открыться файл tsserver.log.

Внимание: журнал TSServer может содержать информацию из вашего рабочего пространства, в том числе пути и исходный код. Если у вас есть какие-либо сомнения относительно его выкладывания на Github, напишите нам, и сможете прислать информацию приватно.
Теги:
Хабы:
+37
Комментарии 4
Комментарии Комментарии 4

Публикации

Информация

Сайт
team.vk.company
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия
Представитель
Руслан Дзасохов