【翻译】只需几步,轻松提高Typescript性能

avatar
前端工程师 @字节跳动

加入我们一起学习,天天向上

原文:github.com/microsoft/T…

豆皮粉儿们,又又又见面了,今天这一期,由字节跳动数据平台的“StoneyAllen ”,给大家翻译一篇文章“typescript性能”。

最近某公司员工在下班途中猝死,引发大家的关注。代码要认真写,但是也不要太过劳累哦,休息好才能工作好!

图片

下面就开始仔细阅读吧!

翻译者:StoneyAllen

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

编写易编译代码

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

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

图片

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

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

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

图片

使用类型注释

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

图片

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

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

图片

但是他们也带来了一定开销。每次将参数传递给 printSchedule 时,需要比较联合类型里的每个元素。对于一个由两个元素组成的联合类型来说,这是微不足道的。但是,如果你的联合类型有很多元素,这将引起编译速度的问题。例如,从联合类型中淘汰多余的部分,元素需要成对的去比较,工作量是呈二次递增的。当大量联合类型交叉一起时发生这种检查,会在每个联合类型上相交导致大量的类型,需要减少这种情况发生。避免这种情况的一种方法是使用子类型,而不是联合类型。

图片

一个更现实的例子是,定义每种内置DOM元素的类型时。这种情况下,更优雅的方式是创建一个包含所有元素的 HtmlElement 基础类型,其中包括 DivElementImgElement 等。使用继承而不是创建一个无穷多的联合类型 DivElement|/*...*/|ImgElement|/*...*/

使用项目引用

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

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

图片

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

图片

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

你可以在这里阅读更多关于项目参考资(www.typescriptlang.org/docs/handbo…

配置tsconfig.json或jsconfig.json

TypeScriptJavaScript用户可以用 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

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

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

图片

控制包含的@types

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

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

图片

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

图片

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

图片

增量项目输出

--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输出需要的语义信息可能不是本地文件。这是为了理解如何输出像 constenumsnamespaces 这样的功能。但是需要检查其他文件来生成某个文件,这会使输出速度变慢。

对需要非本地信息的功能需求是比较少见的--常规枚举可以用来代替 const枚举,模块可以用来代替命名空间。鉴于此,TypeScript提供了 isolatedModules标志,以便在由非本地信息驱动的功能上报错。启用 isolatedModules 意味着你的代码库对于使用 TypeScriptAPIs(如 transpileModule)或替代编译器(如 Babel)的工具是安全的。

举个例子,下面的代码在运行时无法正常使用独立的文件转换,因为 constenum值被期望内联;幸运的是, isolatedModules会在早期告诉我们这一点

图片

记住: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相关的插件),看看是否能解决性能和响应速度方面的问题。

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

诊断扩展

你可以用 --extendedDiagnostics来运行TypeScript,以获得编译器花费时间的打印日志。

图片

请注意,总时间不是前面所有时间的总和,因为有一些重叠,有些工作是没有衡量工具的。

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

图片

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

  • 文件数/代码行数是否与您项目中的文件数大致一致?如果不符合,请尝试运行 --listFiles

  • 程序时间或I/O读取时间是否相当高?请确保你的include/exclude配置正确

  • 其他时间看起来不对劲吗?你可能想提出一个问题。你可以做以下事情来帮助诊断

  • 如果打印时间较高,则使用 emitDeclarationOnly运行

  • 阅读关于报告编译器性能问题的说明

显示配置

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

图片

追踪分辨率

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

图片

如果你发现了一个不应该存在的文件,你可能需要修改你的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选项可以在以下几个方面被滥用

图片

提出问题

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

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

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

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

报告编译器性能问题

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

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

图片

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

  • 安装的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_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. 打开你的命令菜单栏,然后选择

  2. 进入 "首选项 "打开您的全局设置。打开用户设置

  3. 入偏好设置,打开本地项目。打开工作区设置

  4. 设置选项 "typecript.tsserver.log":"verbose"

  5. 重启VS Code,重现问题

  6. 在VS Code中,运行TypeScript。打开TS服务器日志命令

  7. 这将打开tsserver.log文件

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

参考:

只需几步,轻松提高Typescript性能