1

typescript性能(译)

有些简单的Typescript配置,可以让你获得更快的编译和编辑体验,这些方法越早掌握越好。下面列举了除了最佳实践以外,还有一些用于调查缓慢的编译/编辑体验的常用技术,以及一些作为最后手段来帮助TypeScript团队调查问题的常用方法。

编写易编译代码

优先使用接口而不是交叉类型

很多时候,简单对象类型的类型别名与接口的作用非常相似

interface Foo { prop: string }

type Bar = { prop: string }; 

然而,只要你需要定义两个及以上的类型,你就可以选用接口来扩展这些类型,或者在类型别名中对它们相交,这时差异就变得明显了。

由于接口定义的是单一平面对象类型,可以检测属性是否冲突,解决这些冲突是非常必要的。另一方面,交叉类型只是递归的合并属性,有些情况下会产生never。接口则表现的一贯很好,而交叉类型定义的类型别名不能显示在其他的交叉类型上。接口之间的类型关系也会被缓存,而不是整个交叉类型。最后值得注意的区别是,如果是交叉类型,会在检查“有效” /“展平”类型之前检查所有属性。

因此,建议在创建交叉类型时使用带有接口/扩展的扩展类型

- type Foo = Bar & Baz & {

-     someProp: string;

- }

+ interface Foo extends Bar, Baz {

+     someProp: string;

+ } 
使用类型注释

添加类型注释,尤其是返回类型,可以节省编译器的大量工作。这是因为命名类型比匿名类型更简洁(编译器更喜欢),这减少了大量的读写声明文件的时间。虽然类型推导是非常方便的,没有必要到处这么做。但是,如果您知道了代码的慢速部分,可能会很有用

- import { otherFunc } from "other";

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

- export function func() {

+ export function func(): otherType {

      return otherFunc();

  } 
优先使用基础类型而不是联合类型

联合类型非常好用--它可以让你表达一种类型的可能值范围

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 | /.../ 。

使用项目引用

使用TypeScript构建内容较多的代码时,将代码库组织成几个独立的项目会很有用。每个项目都有自己的 tsconfig.json ,可能它会对其他项目有依赖性。这有益于避免在一次编译中导入太多文件,也使某些代码库布局策略更容易地放在一起。

有一些非常基本的方法将一个代码库分解成多个项目。举个例子,一个程序代码,一部分用作客户端,一部分用作服务端,另一部分被其它两个共享

测试也可以分解到自己的项目中

一个常见的问题是 "一个项目应该有多大?"。这很像问 "一个函数应该有多大?"或 "一个类应该有多大?",在很大程度上,这归结于经验。人们熟悉的一种分割JS/TS代码的方法是使用文件夹。作为一种启发式的方法,如果它们关联性足够大,可以放在同一个文件夹中,那么它们就属于同一个项目。除此之外,要避免出现极大或极小规模的项目。如果一个项目比其他所有项目加起来都要大,那就是一个警告信号。同样,最好避免有几十个单文件项目,因为也会增加开销。

你可以在这里阅读更多关于项目参考资料

配置tsconfig.json或jsconfig.json

TypeScript和JavaScript用户可以用tsconfig.json文件任意配置编译方式。JavaScript用户也可以使用jsconfig.json文件配置自己的编辑体验。

指定文件

你应该始终确保你的配置文件没有包含太多文件

在 tsconfig.json 中,有两种方式可以指定项目中的文件

  • files列表
  • include、exclude列表

两者的主要区别是,files期望得到一个源文件的文件路径列表,而include/exclude使用通配符模式对文件进行匹配

虽然指定文件可以让TypeScript直接快速地加载文件,但如果你的项目中有很多文件,而不只是几个顶层的入口,那就会很麻烦。此外,很容易忘记添加新文件到tsconfig.json中,这意味着你可能最终会得到奇怪的编辑器行为,这些新文件被错误地分析,这些都很棘手。

include/exclude有助于避免指定这些文件,但代价是:必须通过include包含的目录来发现文件。当运行大量的文件夹时,这可能会减慢编译速度。此外,有时编译会包含很多不必要的.d.ts文件和测试文件,这会增加编译时间和内存开销。最后,虽然exclude有一些合理的默认值,但某些配置比如mono-repos,意味着像node_modules这样的 "重 "文件夹仍然可以最终被包含。

对于最佳做法,我们建议如下:

  • 在您的项目中只指定输入文件夹(即您想将其源代码包含在编译/分析中的文件夹)
  • 不要把其他项目的源文件混在同一个文件夹里
  • 如果把测试和其他源文件放在同一个文件夹里,请给它们取一个不同的名字,这样就可以很容易地把它们排除在外
  • 避免在源目录中出现大的构建工件和依赖文件夹,如node_modules

注意:如果没有排除列表,默认情况下node_modules是被排除的;一旦添加了node_modules,就必须明确地将node_modules添加到列表中。

下面是一个合理的tsconfig.json,用来演示这个操作

{

    "compilerOptions": {

    // ...

    },

    "include": ["src"],

    "exclude": ["**/node_modules", "**/.*/"],

} 
控制包含的@types

默认情况下,TypeScript会自动包含每一个在node_modules文件夹中找到的@types包,不管你是否导入它。这是为了在使用Node.js、Jasmine、Mocha、Chai等工具/包时,使某些东西 "能够工作",因为这些工具/包没有被导入--它们只是被加载到全局环境中

有时这种逻辑在编译和编辑场景下都会拖慢程序的构建时间,甚至会造成多个全局包的声明冲突的问题,造成类似于如下问题

Duplicate identifier 'IteratorResult'.

Duplicate identifier 'it'.

Duplicate identifier 'define'.

Duplicate identifier 'require'. 

在不需要全局包的情况下,修复方法很简单,只要在 tsconfig.json/jsconfig.json 中为 "type "选项指定一个空字段即可。

// src/tsconfig.json

{

    "compilerOptions": {

        // ...

        // Don't automatically include anything.

        // Only include `@types` packages that we need to import.

        "types" : []

    },

    "files": ["foo.ts"]

} 

如果您仍然需要一些全局包,请将它们添加到类型字段中

// tests/tsconfig.json

{

   "compilerOptions": {

       // ...

       // Only include `@types/node` and `@types/mocha`.

       "types" : ["node", "mocha"]

   },

   "files": ["foo.test.ts"]

} 
增量项目输出

--incremental标志允许TypeScript将上次编译的状态保存到一个 .tsbuildinfo 文件中。这个文件用来计算上次运行后可能被重新检查/重新输出的最小文件集,就像TypeScript的--watch模式一样。

当对项目引用使用复合标志时,默认情况下会启用增量编译,但这样也能带来同样的速度提升。

跳过 .d.ts 检查

默认情况下,TypeScript会对一个项目中的所有.d.ts文件进行全面检查,以发现问题或不一致的地方;然而,这检查通常是不必要的。大多数时候,.d.ts文件都是已知如何工作的--类型之间相互扩展的方式已经被验证过一次,重要的声明还是会被检查。

TypeScript提供了一个选项,使用skipDefaultLibCheck标志来跳过.d.ts文件的类型检查(例如lib.d.ts)

另外,你也可以启用 skipLibCheck 标志来跳过编译中的所有 .d.ts 文件

这两个选项通常会隐藏.d.ts文件中的错误配置和冲突,所以只建议在快速构建场景中使用它们。

使用更快的差异检查

狗的列表是动物的列表吗?也就是说,List<Dog>是否可以分配给List<Animals>?寻找答案的直接方法是逐个成员进行类型结构比较。不幸的是,这可能带来昂贵的性能开销。然而,如果我们对List<T>有足够的了解,我们可以将这个可分配性检查简化为确定Dog,是否可以分配给Animal(即不考虑List<T>的每个成员)。特别是,当我们需要知道类型参数T的差别。编译器只有在启用strictFunctionTypes标志的情况下,才能充分利用这种潜在的加速优势(否则,它就会使用较慢的,但更宽松的结构检查)。因此,我们建议使用 --strictFunctionTypes 来构建(默认在 --strict 下启用)

配置其他构建工具

TypeScript编译经常与其他构建工具一起执行--特别是在编写可能涉及捆绑程序的Web应用程序时。虽然我们只能对一些构建工具提出建议,但理想情况下,这些技术可以被普及。

确保除了阅读本节外,你还阅读了关于你所选择的构建工具的性能--例如:

  • ts-loader的Faster Builds部分
  • awesome-typescript-loader的性能问题部分
并行类型检查

类型检查通常需要从其他文件中获取信息,与转换/输出代码等其他步骤相比,类型检查可能相对昂贵。因为类型检查可能会花费更多的时间,它可能会影响到内部的开发循环--换句话说,你可能会经历更长的编辑/编译/运行周期,这可能会令你头疼。

出于这个原因,一些构建工具可以在一个单独的进程中运行类型检查,而不会阻塞输出。虽然这意味着在TypeScript构建而发生错误报告之前已经有无效的代码运行,通常会先在编辑器中看到错误,而不会被长时间地阻止运行工作代码

一个实际的例子是Webpack的fork-ts-checker-webpack-plugin插件,或者awesome-typescript-loader有时也会这样做。

隔离文件输出

默认情况下,TypeScript输出需要的语义信息可能不是本地文件。这是为了理解如何输出像 const enums 和 namespaces 这样的功能。但是需要检查其他文件来生成某个文件,这会使输出速度变慢。

对需要非本地信息的功能需求是比较少见的--常规枚举可以用来代替const枚举,模块可以用来代替命名空间。鉴于此,TypeScript提供了isolatedModules标志,以便在由非本地信息驱动的功能上报错。启用 isolatedModules 意味着你的代码库对于使用 TypeScript APIs(如 transpileModule)或替代编译器(如 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. 
记住:isolatedModules不会自动让代码生成速度更快--它只是告诉你,你即将使用一个可能不被支持的功能。你要的是独立模块在不同的构建工具和API中的输出

可以通过使用以下工具来影响独立文件的输出

  • ts-loader提供了一个transpileOnly标志,通过使用transpileModule来执行独立文件输出
  • awesome-typescript-loader提供了一个transpileOnly标志,通过使用transpileModule来执行独立文件输出
  • TypeScript可以直接使用transpileModule API
  • awesome-typescript-loader提供了useBabel标志
  • babel-loader以单独的方式编译文件(但不提供类型检查)
  • gulp-typescript 启用 isolatedModules 时,可以实现独立文件输出
  • rollup-plugin-typescript只执行独立文件编译
  • ts-jest可以使用( isolatedModules标志设为true )isolatedModules为true
  • ts-node 可以检测 tsconfig.json 的 "ts-node "字段中的 "transpileOnly "选项,也有一个 --transpile-only 标志。

调查问题

有一定的方法可以得到可能出问题的提示

禁用编辑器插件

编辑器的体验受到插件的影响。尝试禁用插件(尤其是JavaScript/TypeScript相关的插件),看看是否能解决性能和响应速度方面的问题。

某些编辑器也有自己的性能故障排除指南,所以可以考虑阅读一下。例如,Visual Studio Code也有自己的性能问题介绍。

诊断扩展

你可以用--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 
请注意,总时间不是前面所有时间的总和,因为有一些重叠,有些工作是没有衡量工具的。

对于大多数用户来说,最相关的信息是:

FieldMeaning
Filesthe number of files that the program is including (use --listFiles to see what they are).
I/O Read timetime spent reading from the file system - this includes traversing include'd folders.
Parse timetime spent scanning and parsing the program
Program timecombined time spent performing reading from the file system, scanning and parsing the program, and other calculation of the program graph. These steps are intermingled and combined here because files need to be resolved and loaded once they're included via imports and exports.
Bind timeTime spent building up various semantic information that is local to a single file.
Check timeTime spent type-checking the program.
transformTime timeTime spent rewriting TypeScript ASTs (trees that represent source files) into forms that work in older runtimes.
commentTimeTime spent calculating comments in output files.
I/O Write timeTime spent writing/updating files on disk.
printTimeTime spent calculating the string representation of an output file and emitting it to disk.

考虑到这些投入,你可能会想问一些问题:

  • 文件数/代码行数是否与您项目中的文件数大致一致?如果不符合,请尝试运行--listFiles
  • 程序时间或I/O读取时间是否相当高?请确保你的include/exclude配置正确
  • 其他时间看起来不对劲吗?你可能想提出一个问题。你可以做以下事情来帮助诊断

    • 如果打印时间较高,则使用emitDeclarationOnly运行
    • 阅读关于报告编译器性能问题的说明
显示配置

当运行 tsc 时,并不能明显地看到编译的内容设置,特别是考虑到 tsconfig.jsons 可以扩展其他配置文件。showConfig 可以解释 tsc 将为一个调用计算着什么。

tsc --showConfig

# or to select a specific config file...

tsc --showConfig -p tsconfig.json 
追踪分辨率

运行 traceResolution 可以有助于解释,一个文件为什么被包含在编译中。输出有点繁琐,所以你可能想把输出重定向到一个文件。

tsc --traceResolution > resolution.txt 

如果你发现了一个不应该存在的文件,你可能需要修改你的tsconfig.json中的include/exclude列表,或者,你可能需要调整其他设置,比如type、typeRoots或paths

独立运行tsc

很多时候,用户在使用第三方构建工具(如Gulp、Rollup、Webpack等)时都会遇到性能缓慢的问题。运行tsc --extendedDiagnostics,可以发现TypeScript和工具之间的差异,用以说明外部配置的错误或效率低下。

一些需要注意的问题:

  • tsc和集成了TypeScript的构建工具在构建时间上有很大的区别吗?
  • 如果构建工具提供诊断,那么TypeScript的分辨率和构建工具的分辨率是否有区别?
  • 构建工具是否有自己的配置,可能的原因是什么?
  • 构建工具是否有可能是TypeScript集成的配置原因?(例如ts-loader的选项?)
升级依赖性

有时TypeScript的类型检查会受到计算密集的.d.ts文件的影响。这很罕见也很可能会发生。升级到一个较新的TypeScript版本(可以更有效率)或一个较新版本的@types包(可能已经恢复了一个回归)通常可以解决这个问题。

常见的问题

一旦你已经排除了故障,你可能想探索一些常见问题的修复方法。如果以下解决方案不起作用,可能值得提出问题。

include和exclude配置不当

如上所述,include/exclude选项可以在以下几个方面被滥用

ProblemCauseFix
node_modules was accidentally included from deeper folderexclude was not set"exclude": ["/node_modules", "/.*/"]
node_modules was accidentally included from deeper folder"exclude": ["node_modules"]"exclude": ["/node_modules", "/.*/"]
Hidden dot files (e.g. .git) were accidentally included"exclude": ["**/node_modules"]"exclude": ["/node_modules", "/.*/"]
Unexpected files are being included.include was not set"include": ["src"]

提出问题

如果你的项目已经进行了正确的优化配置,你可能需要提出一个问题。

最好的性能问题报告包含容易获得的和最小的问题复制品。换句话说,一个容易通过git克隆的代码库,只包含几个文件。它们不需要与构建工具的外部集成--它们可以通过调用tsc或调用TypeScript API的独立代码。不优先考虑那些需要复杂调用和设置的代码库。

我们理解这一点却不容易实现--特别是,很难在代码库中隔离问题的源头,而且共享知识产权可能也是一个问题。在某些情况下,如果我们认为问题影响较大,团队将愿意发送一份保密协议(NDA)。

无论是否可以复制,在提交问题时,按照这些方法,将有助于为您提供性能修复。

报告编译器性能问题

有时,你会在构建时间以及编辑场景中发现性能问题。在这种情况下,最好关注于TypeScript编译器。

首先,应该使用TypeScript的next版本,以确保你不会碰到那些已解决的问题。

npm install --save-dev typescript@next

# or

yarn add typescript@next --dev 

一个编译器的问题可能包括

  • 安装的TypeScript版本(例如:npx tsc -v 或 yarn tsc -v)
  • TypeScript运行的Node版本(例如:node -v)
  • 使用extendedDiagnostics运行的输出(tsc --extendedDiagnostics -p tsconfig.json)
  • 理想的情况是,一个项目能够展示所遇到的问题
  • 剖析编译器的输出日志(isolate---.log 和.cpuprofile 文件)
剖析编译器

通过使用--trace-ic标志与--generateCpuProfile标志,来让TypeScript运行Node.js v10+,这对团队提供诊断结果来说是很重要的:

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上公开发布这些有任何顾虑,请告诉我们,可以私下分享细节。

报告编辑绩效问题

编辑性能经常受到很多东西的影响,TypeScript团队唯一能控制的是JavaScript/TypeScript语言服务的性能,以及该语言服务和某些编辑器(即Visual Studio、Visual Studio Code、Visual Studio for Mac和Sublime Text)之间的集成。确保所有第三方插件在编辑器中被关闭,以确定是否有TypeScript本身的问题。

编辑性能问题稍有涉及,但同样的想法也适用于:可被克隆的最小重现代码库是理想的,虽然在某些情况下,团队将能够签署NDA来调查和隔离问题。

包括tsc--extendedDiagnostics的输出是很好的上下文,但取一个TSServer日志是最有用的。

收集TSServer日志
在Visual Studio代码中收集TSServer日志
  1. 打开你的命令调色板,然后选择

    1. 进入 "首选项 "打开您的全局设置。打开用户设置
    2. 入偏好设置,打开本地项目。打开工作区设置
  2. 设置选项 "typecript.tsserver.log":"verbose"
  3. 重启VS Code,重现问题
  4. 在VS Code中,运行TypeScript。打开TS服务器日志命令
  5. 这将打开tsserver.log文件

⚠警告:TSServer日志可能会包含你的工作空间的信息,包括文件路径和源代码。如果你对在GitHub上公开发布有任何顾虑,请告诉我们,你可以私下分享细节。


石坚
413 声望14 粉丝