原文:https://github.com/microsoft/...
豆皮粉儿们,又又又见面了,今天这一期,由字节跳动数据平台的“StoneyAllen
”,给大家翻译一篇文章“typescript性能”。在2020年倒计时最后几天我要祝大家新年快乐~,打工人,打工魂~。下面就开始仔细阅读吧!
翻译者:StoneyAllen
有些简单的Typescript配置,可以让你获得更快的编译和编辑体验,这些方法越早掌握越好。下面列举了除了最佳实践以外,还有一些用于调查缓慢的编译/编辑体验的常用技术,以及一些作为最后手段来帮助TypeScript团队调查问题的常用方法。
编写易编译代码
优先使用接口而不是交叉类型
很多时候,简单对象类型的类型别名与接口的作用非常相似
然而,只要你需要定义两个及以上的类型,你就可以选用接口来扩展这些类型,或者在类型别名中对它们相交,这时差异就变得明显了。
由于接口定义的是单一平面对象类型,可以检测属性是否冲突,解决这些冲突是非常必要的。另一方面,交叉类型只是递归的合并属性,有些情况下会产生never。接口则表现的一贯很好,而交叉类型定义的类型别名不能显示在其他的交叉类型上。接口之间的类型关系也会被缓存,而不是整个交叉类型。最后值得注意的区别是,如果是交叉类型,会在检查“有效” /“展平”类型之前检查所有属性。
因此,建议在创建交叉类型时使用带有接口/扩展的扩展类型。
使用类型注释
添加类型注释,尤其是返回类型,可以节省编译器的大量工作。这是因为命名类型比匿名类型更简洁(编译器更喜欢),这减少了大量的读写声明文件的时间。虽然类型推导是非常方便的,没有必要到处这么做。但是,如果您知道了代码的慢速部分,可能会很有用。
优先使用基础类型而不是联合类型
联合类型非常好用--它可以让你表达一种类型的可能值范围。
但是他们也带来了一定开销。每次将参数传递给 printSchedule
时,需要比较联合类型里的每个元素。对于一个由两个元素组成的联合类型来说,这是微不足道的。但是,如果你的联合类型有很多元素,这将引起编译速度的问题。例如,从联合类型中淘汰多余的部分,元素需要成对的去比较,工作量是呈二次递增的。当大量联合类型交叉一起时发生这种检查,会在每个联合类型上相交导致大量的类型,需要减少这种情况发生。避免这种情况的一种方法是使用子类型,而不是联合类型。
一个更现实的例子是,定义每种内置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
,用来演示这个操作
控制包含的@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输出需要的语义信息可能不是本地文件。这是为了理解如何输出像 const enums
和 namespaces
这样的功能。但是需要检查其他文件来生成某个文件,这会使输出速度变慢。
对需要非本地信息的功能需求是比较少见的--常规枚举可以用来代替const
枚举,模块可以用来代替命名空间。鉴于此,TypeScript提供了isolatedModules
标志,以便在由非本地信息驱动的功能上报错。启用 isolatedModules
意味着你的代码库对于使用 TypeScript APIs
(如 transpileModule
)或替代编译器(如 Babel
)的工具是安全的。
举个例子,下面的代码在运行时无法正常使用独立的文件转换,因为const enum
值被期望内联;幸运的是, 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相关的插件),看看是否能解决性能和响应速度方面的问题。
某些编辑器也有自己的性能故障排除指南,所以可以考虑阅读一下。例如,Visual Studio Code
也有自己的性能问题介绍。
诊断扩展
你可以用--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日志
打开你的命令菜单栏,然后选择
- 进入 "首选项 "打开您的全局设置。打开用户设置
- 入偏好设置,打开本地项目。打开工作区设置
- 设置选项 "typecript.tsserver.log":"verbose"
- 重启VS Code,重现问题
- 在VS Code中,运行TypeScript。打开TS服务器日志命令
- 这将打开tsserver.log文件
⚠警告:TSServer日志可能会包含你的工作空间的信息,包括文件路径和源代码。如果你对在GitHub上公开发布有任何顾虑,请告诉我们,你可以私下分享细节。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。