[Typescript] 성능 고려하기

Dongmin Jang
12 min readDec 12, 2020
이렇게 하나로 다 합쳐 놓으면 먹기는 좋다

다음 글에 대한 번역&요약입니다.

컴파일 하기 쉽게 코드 작성하기

Intersection 보다 Interface 선호

보통은 object type에 대한 간단한 type alias는 interface와 아주 비슷하게 동작합니다.

interface Foo { prop: string };
type Bar = { prop: string };

그러나 두개 혹은 그 이상을 작성해야 하는 순간, interface로 이러한 type을 확장하거나, type alias에서 교차하는 방법이 있습니다. 여기서 차이가 중요해지기 시작합니다.

interface는 일반적으로 해결 해야하는 충돌을 감지하는 단일 flat object를 만듭니다. 반면에 intersection은 재귀적으로 속성을 병합하고 일부는 생성하지 않습니다. interface 또한 더 일관성있게 표시합니다. 반면에 intersection에 대한 type alias는 다른 intersection의 일부에 표시될 수 없습니다. 전반적으로 intersection이 아닌 interface간의 type 관계는 캐시 됩니다. 마지막으로 주목해야 될 차이는 대상이 되는 intersection type을 검사할 때, ‘effective’/’flattened’ type에 대해서 검사하기 전에 모든 요소를 검사합니다.

이러한 이유 때문에 interface/extends를 이용한 type 확장을 intersection type보다 추천합니다.

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

Type Annotations 사용

type annotation을 추가하는 것은 (특히 return type)컴파일러의 과도한 작동을 피할 수 있습니다. 부분적으로 이것은 명명된 type이 익명 type(컴파일러가 추론할 수 있는)보다 더 간결한 경향이 있기 때문에 선언된 파일의(incremental builds) 읽고 쓰는데 소요되는 시간이 줄어듭니다. type 추론은 아주 편리하므로, 보편적으로 사용될 필요는 없지만 코드의 느린 부분을 인식한 경우 시도해 보는 것이 좋습니다.

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

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

Union보다는 기본 type을 선호

Union type은 아주 훌륭합니다. type에 대해서 값의 유효 범위를 표현할 수 있게 해줍니다.

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을 넘겨 줄 것입니다. 이것은 union의 각각의 요소들을 비교해야 합니다. 두 union 요소에게는 사소하고 그리 비싸지 않은 비용입니다. 그러나 union 이 더 많아지면 실제로 컴파일 속도에 악영향을 끼치게 됩니다. 예를 들어, union에서 중복 요소를 제거하기 위해 요소를 쌍으로 비교해야 합니다.(2차) 이러한 종류의 검사는 큰 union의 교차 할 때 발생할 수 있습니다. 각각의 union 구성 요소가 교차하는 부분은 감소되야만 하는 엄청난 크기의 type이 생성 될 수 있습니다. 이를 피하는 방법은 subtype을 사용하는 것입니다.

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 element type을 모델링 하려고 할 때 입니다. 이 경우에는 거대한 DivElement | /*...*/ | ImgElement | /*...*/ union 을 만드는 것보다DivElement, ImgElement 같은 공통 요소와 함께 기본 HtmlElement을 만드는게 낫습니다.

프로젝트 참조 사용

TypeScript를 이용해서 작지 않은 코드베이스를 만들 때, 몇몇 독립적인 프로젝트를 구성하는 것이 유용합니다. 각각의 프로젝트는 다른 프로젝트에 종속적인 tsconfig.json 을 가지고 있습니다. 이는 단일 컴파일에서 너무 많은 파일을 내려 받는 것을 피하는데 도움을 줍니다. 또한 특정 코드베이스 레이아웃 전략을 쉽게 통합할 수 있게합니다.

프로젝트에서 코드베이스를 나누는 기본 전략들이 있습니다. 예를 들면, client용 프로젝트, server용 프로젝트 그리고 그 둘간에 공유되는 프로젝트가 될 수 있습니다.

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

테스트는 자체 프로젝트로 나뉠 수 있습니다.

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

자주 묻는 질문 중 하나는 프로젝트는 얼마나 커야 하냐는 것 입니다. 이것은 다음 질문과 비슷합니다. “함수는 얼마나 커야 합니까?” 또는 “클래스는 얼마나 커야 합니까?” 그리고 대부분은 경험으로 귀결됩니다. js/ts코드를 분리하는 가장 익숙한 방법은 폴더를 나누는 것입니다. 경험적으로 연관이 많으면 한 폴더에 들어가고, 같은 프로젝트에 포함 됩니다. 그외에도 대규모 프로젝트는 피하세요. 만약 다른 프로젝트를 합친 것보다 한 프로젝트가 더 크다면 매우 위험하다는 신호 입니다. 마찬가지로 수십개의 단일 파일 프로젝트는 피하는게 좋습니다. 오버헤드를 증가시키기 때문입니다.

project reference 를 읽어 보세요..

tsconfig.json 또는 jsconfig.json 구성

typescript & javascript 유저는 항상 tsconfig.json파일을 이용해서 컴파일 구성을 할 수 있습니다. jsconfig.json 파일은 js 유저의 작업 환경을 설정하는데 사용 될 수 있습니다.

파일 지정

설정 파일에 너무 많은 파일이 한번에 포함되지 않도록 해야 합니다.

프로젝트에서 tsconfig.json 파일 안에 파일들을 명시하는 방법에는 두가지가 있습니다.

  • the files list
  • the include and exclude lists

이 두가지에 가장 큰 차이점은 파일이 소스파일의 경로를 예상하고, include/exclude를 사용하여 파일에 일치되는 globbing patterns 을 사용한다는 것입니다.

파일을 지정하면 typescript가 빠르게 직접 파일을 처리할 수 있지만, 프로젝트에서 top-level의 진입점이 없이 많은 파일이 있다면 매우 느릴 수 있습니다. 추가적으로 tsconfig.json에 새 파일의 경로를 추가하는 것을 잊을 수 있습니다. 이는 새 파일을 잘못 분석하여 잘못된 결과를 맞닿들일 수 있습니다. 이러한 모든것이 번거로울 수 있습니다.

include/exclude 는 파일들을 명시할 필요가 없도록 해줍니다. 그러나 비용이 발생합니다. 포함하고 있는 디렉토리를 모두 검색해서 파일을 찾아야 합니다. 너무 많은 폴더를 검색하면 느려질 수 있습니다. 이는 컴파일 속도를 낮출 수 있습니다. 또한 때때로 컴파일은 불필요한 .d.ts 파일들과 테스트 코드를 포함 할 수 있는데, 이는 컴파일 시간과 메모리 오버헤드를 증가 시킵니다. 마지막으로 exclude는 합리적인 기본값을 가지고 있지만, mono-repo와 같은 특정 구성은 node_modules와 같은 무거운 폴더를 포함 할 수 있다는 것을 의미 합니다.

가장 좋은 연습 방법으로는 다음을 추천합니다.

  • 프로젝트의 input 폴더만 명시합니다 (컴파일 / 분석을 위해 포함시키고자 하는 폴더)
  • 한 폴더에 다른 프로젝트의 소스 파일을 섞지 마세요
  • 만약 같은 폴더에서 다른 소스파일의 테스트 파일을 가지고 있다면 명확한 이름을 지정하고 제외 될 수 있도록 하세요.
  • 소스 디렉토리안에서 node_modules와 같은 large build artifacts and dependency folders는 피하세요.

Note: exclude 리스트가 없다면 node_modules는 기본으로 제외 됩니다. 하나만이라도 추가된다면, 제외 리스트에 node_modules를 명확하게 추가해야 합니다.

아래는 위 내용을 예로 보여주는 tsconfig.json 입니다.

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

@types 포함 제어

기본적으로 Typescript는 import 하는지 여부와 상관없이 node_modules에서 찾은 모든 @types 패키지를 포함합니다. 이것은 node.js, jasmine, mocha, chai등에서 그냥 작동하게 하기 위함입니다.이러한 툴/패키지는 import 하지 않고 global 환경에서 사용하기 때문입니다.

때로는 이러한 로직이 컴파일이나 수정할 때, 프로그램 구동 시간을 낮출 수 있습니다. 그리고 선언이 출동하여 여러 global 패키지들에서 이슈가 생길 수 있습니다.

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

global 패키지가 필요하지 않는 경우, tsconfig.json/jsconfig.json의 types option에 empty field를 명시하면 됩니다.

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

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

global 패키지가 필요한게 있다면 type field에 넣어주면 됩니다.

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

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

Incremental Project Emit

--incremental flag는 typescript가 마지막 컴파일 state를 .tsbuildinfo 에 저장할 수 있게 해줍니다. 이 파일은 typescript의 watch 모드가 동작하듯이 마지막 실행 이후에 다시 확인/내보내기를 할 수 있는 가장 작은 파일 집합을 계산하는데 사용됩니다.

증분 컴파일은 프로젝트 참조에서 composite flag를 사용할 때 기본적으로 활성화 되어있습니다만, 옵트인 하는 모든 프로젝트에똑같이 속도 향상을 가져옵니다.

.d.ts 검사 거르기

기본적으로 typescript 는 이슈와 불일치를 찾기위해 프로젝트에서 .d.ts 를 전부 검사하는 것을 수행합니다. 그러나 이것은 일반적으로 불필요 합니다. 대부분의 경우 .d.ts 파일은 이미 작동하는 것으로 알려져 있습니다. type들이 서로 확장하는 방식으로 이미 한번 확인 되었고, 선언은 어떻게든 확인 됩니다.

typescript는skipDefaultLibCheck flag를 사용하여 .d.ts 파일의 type 확인을 거르는 옵션을 제공합니다.

또는 skipLibCheck flag를 사용하여 모든 .d.ts. 파일의 컴파일을 거를 수 있습니다.

이 두 옵션은 .d.ts 파일의 잘못 된 구성과 충돌을 감출 수 있기 때문에 꼭 빠른 빌드가 필요할 때만 사용하길 권장합니다.

빠른 분산 검사

개 목록이 동물 목록인가요? 그러니까 List<Dog>List<Animals> 에 할당 할 수 있나요? 알 수 있는 간단한 방법은 type의 멤버간에 구조적 비교 입니다. 불행히도 이는 비용이 비쌉니다. 그러나 List<T> 에 대해서 알고 있다면, 개를 동물에 할당 할 수 있는지 여부 확인을 줄일 수 있습니다. ( type parameter T에 대해서 좀 알아야 합니다) strictFunctionTypes flag가 활성활 되어 있을 때, 컴파일러는 잠재적인 속도 향상의 이점을 활용할 수 있습니다. (그렇지 않으면 더 천천히 관대한 구조적 검사를 진행합니다) 이러한 이유 때문에 --strictFunctionTypes 를 이용해서 빌드하기를 추천합니다. (strict 모드에서 기본으로 활성화 됩니다)

기타 빌드 도구 구성

Typescript 컴파일은 종종 다른 빌드툴과 수행 됩니다. 특히 번들러를 포함하는 웹앱을 작성할 때 입니다. 몇몇 빌드툴에 대해서는 제안 할 수 있는데 이러한 기술들이 일반화 될 수도 있습니다.

게다가 이 섹션을 읽는 것 외에도 당신이 선택한 빌드툴에 성능에 대해서 읽어야 합니다. 예를 들면..

Type 검사 동시성

type 검사는 일반적으로 다른 파일의 정보가 필요합니다. 그리고 변형/전송과 같은 다른 단계에 비해 상대적으로 비용이 비쌀 수 있습니다. type 검사가 오래 걸릴 수 있기 때문에 내부 개발 loop 에 영향을 미칠 수 있습니다. 즉 수정/컴파일/실행 싸이클이 오래 걸리는 경험을 할 수 있고, 이는 당신을 실망 시킬 것입니다.

이러한 이유 때문에 일부 빌드툴은 emit 을 막지 않고 분리하여 type 검사를 실행할 수 있습니다. 이는 빌드툴에서 typescript가 에러를 보고하기전에 유효하지 않은 코드가 실행 될 수 있게 하지만, 종종 편집기에서 먼저 오류를 보게 되며 작동 코드 실행에 긴 시간 차단 되지 않을 것입니다.

위에 대한 예는 fork-ts-checker-webpack-plugin webpack plugins 입니다. 또는 awesome-typescript-loader 도 종종 비슷하게 동작합니다.

격리된 파일 방출

기본적으로 typescript의 emit은 파일에 로컬이 아닐 수 있는 의미론적 정보를 요구합니다. 이는 const enum과 namespace 처럼 emit 기능을 어떻게 처리하는지 이해하기 위함입니다. 그러나 임의의 파일에 대해 output 을 생성하기 위해 다른 파일을 검사해야 하는 것은 emit을 느려지게 할 수 있습니다.

로컬이 아닌 정보를 필요로 하는 기능에 대한 필요성은 다소 희귀합니다. 일반 enum은 const enum을 대신해서 사용될수 있습니다. 그리고 모듈은 namespace를 대신해서 사용될 수 있습니다. 이러한 이유들 때문에 typescript는 로컬이 아닌 정보로 부터 작동되는 기능들의 에러를 위해 isolatedModules flag를 제공합니다. isolatedModules 활성화 한다는 것은 당신의 코드베이스는 transpileModule 과 같은 typescript api나 babel과 같은 대체 컴파일러를 사용하는 툴에서 안전하다는 것을 의미합니다.

예를 들면, 다음 코드는 런타임시에 고립된 파일 변형과 함께 제대로 동작하지 않을 것입니다. 왜냐하면 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.

Remember: isolatedModules 자동으로 코드 생성을 빠르게 만들어 주지 않습니다.지원되지 않는 기능을 사용하려고 할때만 알려줍니다. 당신이 찾고 있는 것은 다른 빌드툴과 api에서 격리 된 module emit 입니다.

격리된 file emit 은 아래 툴을 사용하여 이점이 될 수있습니다.

문제 조사

무엇이 잘못 될지 힌트를 얻을 방법이 있습니다.

Disabling Editor Plugins

에디터 경험은 plugins에 영향 받을 수 있습니다. 성능과 반응성에 관련된 이슈들이 해결 되는지 plugins를 꺼보세요.

일부 에디터는 성능을 위한 트러블 슈팅 가이드도 가지고 있으니, 읽어보세요. 예를 들면, vscode는 Performance Issues 에 대한 자체 페이지를 가지고 있습니다.

extendedDiagnostics (확장 진단)

--extendedDiagnostics 옵션과 함께 typescript를 실행할 수 있습니다. 컴파일러가 어디에 시간을 많이 쓰는지 출력이 됩니다.

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 은 이전의 모든 시간의 합이 아닙니다. 일부 겹쳐지는 것도 있고, 측정 되지 않는 것도 있습니다.

대부분의 사용자에게 가장 관련 있는 정보는 다음과 같습니다.

이에 대해 궁금한 것들:

showConfig

tsc가 동작할 때, 컴파일이 함께 동작하도록 구성하는 것은 명확하지 않습니다. 특히 tsconfig.json 파일이 다른 구성 파일을 확장할 수 있다는 것을 고려하면 말입니다. showConfig 는 tsc가 실행에 관해 계산하는 것을 설명할 수 있습니다.

tsc --showConfig

# or to select a specific config file...

tsc --showConfig -p tsconfig.json

traceResolution

traceResolution 와 함께 실행하는 것은 왜 파일이 컴파일에 포함이 되는지를 설명하는데 도움이 됩니다. emit 은 다소 장황하므로 출력을 다른 파일로 할 수 있습니다.

tsc --traceResolution > resolution.txt

존재하지 않는 파일을 찾아야 한다면, tsconfig.json 안에 include/exclude 목록을 찾아보거나 다른 types/typeRoots/paths 설정을 적용해봐야 합니다.

Running tsc Alone

많은 경우 사용자는 gulp, rollup, webpack 등과 같은 다른 빌드툴을 사용하여 성능을 떨어뜨립니다. tsc --extendedDiagnostics 로 실행하여 typescript 와 도구 사이의 불일치성을 찾아내는 것은 외부의 잘못된 구성과 비효율성을 나타낼 수 있습니다.

명심해야 될 질문들:

  • tsc 와 typescript 연동에 사용하는 빌드툴간에 빌드 시간에서 큰 차이가 있나요?
  • 빌드툴이 진단을 제공할 경우, typescript의 진단과 다른 빌드툴간의 차이가 있나요?
  • 빌드툴에 원인이 될 수 있는 자체 구성이 있나요?
  • 빌드툴은 원인이 될 수 있는 typescript 연동을 위한 구성을 가지고 있나요?(e.g. options for ts-loader?)

Upgrading Dependencies

때로는 typescript의 type 검사는 계산 집약적으로 .d.ts 파일에 영향을 받을 수 있습니다. 이는 드물지만 발생 할 수 있습니다. 최신 버전의 typescript나 @types로 업그레이드 하면 이 문제를 해결 할 수 있습니다.

Common Issues

트러블 슈팅을 할 때, 몇몇 기본 이슈들에 대한 해결 방법을 찾고 싶을 것입니다. 만약 해결책이 작동하지 않는다면, 문제 제기할 필요가 있습니다.

include and exclude 잘못된 구성

위에서 언급한 것처럼 include/exclude 옵션이 아래처럼 잘못 사용 될 수 있습니다.

Filing an Issue

만약 프로젝트가 이미 적절히 최적화 되어 있다면, 이슈 등록을 할 수도 있습니다.

성능 이슈의 최고의 보고는 쉽게 얻을 수 있고, 최소한의 문제 재현이 포함 됩니다. 즉, git을 통해 쉽게 복제 할 수 있는 코드를 말합니다. 빌드 툴과 외부 통합을 필요로 하지 않습니다. tsc를 통해 호출하거나 typescript api를 사용하는 코드를 사용할 수 있습니다. 실행이나 설정이 복잡한 코드는 우선 순위에서 배제될 수 있습니다.

We understand that this is not always easy to achieve — specifically, because it is hard to isolate the source of a problem within a codebase, and because sharing intellectual property may be an issue. In some cases, the team will be willing to send a non-disclosure agreement (NDA) if we believe the issue is highly impactful.

Regardless of whether a reproduction is possible, following these directions when filing issues will help us provide you with performance fixes.

Reporting Compiler Performance Issues

Sometimes you’ll witness performance issues in both build times as well as editing scenarios. In these cases, it’s best to focus on the TypeScript compiler.

First, a nightly version of TypeScript should be used to ensure you’re not hitting a resolved issue:

npm install --save-dev typescript@next

# or

yarn add typescript@next --dev

A compiler perf issue should include

  • The version of TypeScript that was installed (i.e. npx tsc -v or yarn tsc -v)
  • The version of Node on which TypeScript ran (i.e. node -v)
  • The output of running with extendedDiagnostics (tsc --extendedDiagnostics -p tsconfig.json)
  • Ideally, a project that demonstrates the issues being encountered.
  • Output logs from profiling the compiler (isolate-*-*-*.log and *.cpuprofile files)

Profiling the Compiler

It is important to provide the team with diagnostic traces by running Node.js v10+ with the --trace-ic flag alongside TypeScript with the --generateCpuProfile flag:

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

Here ./node_modules/typescript/lib/tsc.js can be replaced with any path to where your version of the TypeScript compiler is installed, and tsconfig.json can be any TypeScript configuration file. profile.cpuprofile is an output file of your choice.

This will generate two files:

  • --trace-ic will emit to a file of the isolate-*-*-*.log (e.g. isolate-00000176DB2DF130-17676-v8.log).
  • --generateCpuProfile will emit to a file with the name of your choice. In the above example, it will be a file named profile.cpuprofile.

⚠ Warning: These files may include information from your workspace, including file paths and source code. Both of these files are readable as plain-text, and you can modify them before attaching them as part of a GitHub issue. (e.g. to scrub them of file paths that may expose internal-only information).

However, if you have any concerns about posting these publicly on GitHub, let us know and you can share the details privately.

Reporting Editing Performance Issues

Perceived editing performance is frequently impacted by a number of things, and the only thing within the TypeScript team’s control is the performance of the JavaScript/TypeScript language service, as well as the integration between that language service and certain editors (i.e. Visual Studio, Visual Studio Code, Visual Studio for Mac, and Sublime Text). Ensure that all 3rd-party plugins are turned off in your editor to determine whether there is an issue with TypeScript itself.

Editing performance issues are slightly more involved, but the same ideas apply: clone-able minimal repro codebases are ideal, and though in some cases the team will be able to sign an NDA to investigate and isolate issues.

Including the output from tsc --extendedDiagnostics is always good context, but taking a TSServer trace is the most helpful.

Taking a TSServer Log

Collecting a TSServer Log in Visual Studio Code

  1. Open up your command palette and either
    - open your global settings by entering Preferences: Open User Settings
    - open your local project by entering Preferences: Open Workspace Settings
  2. Set the option "typescript.tsserver.log": "verbose",
  3. Restart VS Code and reproduce the problem
  4. In VS Code, run the TypeScript: Open TS Server log command
  5. This should open the tsserver.log file.

⚠ Warning: A TSServer log may include information from your workspace, including file paths and source code. If you have any concerns about posting this publicly on GitHub, let us know and you can share the details privately.

--

--