打字稿编译速度 - 尝试解决方法但坚持合并

时间:2013-10-30 09:05:26

标签: typescript

我在过去3个月里一直在使用Typescript来创建非常复杂的CRUD应用程序。 Typescript提供的编译时安全性为我的工作提供了显着的加速 - 在编译时捕获错误是一个天赐之物,而不是看到它们在运行时表现为异常和不当行为。

但是有一个问题。

我必须处理数百个表,所以我使用的是从DB模式开始的自定义代码生成器,并自动生成大量的Typescript文件。只要架构很小,这就完美了 - 但对于包含数百个表的非常大的模式,tsc的编译时间正在成为一个问题 - 我看到一组400个文件的编译时间为15分钟...(以及“CALL_AND_RETRY_2分配失败”的可怕编译错误 - 即内存不足问题......)

到目前为止,我一直在Makefile中使用tsc,使用“tsc --out ...”语法调用它,从我的所有.ts文件生成一个.js。因此,我认为我可以通过以增量方式进行构建来解决这个问题:自己编译每个.ts(即,在时间只将一个.ts文件传递给tsc)并最终连接所有生成的.js在一个单一的。这确实有效 - 只有在正常开发期间才需要重新编译已更改的文件(并且只有初始编译通过所有这些文件,因此需要花费很多时间)。

但事实证明,这也有一个问题:为了使每个.ts“独立可编译”,我必须在顶部添加所有相关的依赖项 - 也就是说,

这样的行
/// <reference path=...

...在每个.ts文件的顶部。

事实证明,由于这些引用,生成的.js文件包含相同的部分,这些部分在其中许多部分重复...所以当我连接.js文件时,我得到了相同函数的多个定义,更糟糕的是,全局范围语句(var global = new ...)重复了!

因此,我需要一种方法以某种方式智能地“合并”生成的.js文件,以避免看到复制的函数定义......

有没有办法以聪明的方式进行合并,避免重复?或者可能还有其他一些加速编译的方法?

任何建议最受欢迎... tsc编译速度比普通编译器慢30-100倍 - 现在它确实是一个阻塞点。

更新,2天后

Basarat(见下面的答案)帮助我在我的项目中应用他的解决方案。事实证明,尽管他的解决方案与小型和平均大小的项目完美配合,但我得到了可怕的“致命错误:CALL_AND_RETRY_2分配失败 - 处理内存不足”错误 - 这与我使用“tsc时遇到的错误相同 - 出......“。

最后,我的基于Makefile的解决方案是唯一有效的方法 - 按照这样做:

%.js:   %.ts
    @UPTODATE=0 ;                                                          \
    if [ -f "$<".md5 ] ; then                                              \
            md5sum -c "$<".md5 >/dev/null 2>&1 && {                        \
                    UPTODATE=1 ;                                           \
            } ;                                                            \
    fi ;                                                                   \
    if [ $$UPTODATE -eq 0 ] ; then                                         \
            echo Compiling $<  ;                                           \
            tsc --sourcemap --sourceRoot /RevExp/src/ --target ES5 $< || { \
                    rm $@ "$<".md5 ;                                       \
                    exit 1 ;                                               \
            } ;                                                            \
            md5sum "$<" > "$<".md5 ;                                       \
    fi

...它做两件事:它使用MD5校验和来确定何时实际执行编译,并以“独立”方式进行编译(即没有tsc的“--out”选项)。

在实际的目标规则中,我曾经合并了生成的.js文件......但是这让我没有工作.map文件(用于调试) - 所以我现在在index.html生成直接包含:

${WEBFOLDER}/index.html:        $(patsubst %.ts,%.js,${CONTROLLERS_SOURCES}) ${WEBFOLDER}/index.html.template
    cat ${WEBFOLDER}/index.html.template > $@ || exit 1
    REV=$$(cat revision) ;                                                                                      \
    for i in $(patsubst %.ts,%.js,${CONTROLLERS_SOURCES}) ; do                                                  \
        BASE=$$(basename $$i) ;                                                                                 \
        echo "      <script type='text/javascript' src='js/$${BASE}?rev=$$REV'></script>" >> $@ ;              \
    done || exit 1
    cat RevExp/templates/index.html.parallel.footer >> $@ || exit 1
    cp $(patsubst %.ts,%.js,${CONTROLLERS_SOURCES}) ${WEBFOLDER}/js/ || exit 1

我将把这个问题留待未来的贡献......

3 个答案:

答案 0 :(得分:4)

我有一个可以管理你的打字稿项目的咕噜插件:https://github.com/basarat/grunt-ts

我看到大约250个文件的编译时间为6秒。这是一个使用grunt-ts的视频教程:http://www.youtube.com/watch?v=0-6vT7xgE4Y&hd=1

答案 1 :(得分:2)

我的项目中已经达到了300多个* .ts文件,而且我遇到了0.9.1.1编译器(较新版本使用了TypeScript的不兼容版本,我不得不执行大量重构以使编译器满意)编译时间约为25秒。 我使用tsc app.ts --out app.js

我得到tsc app.ts的类似时间,即不使用--out app.js而是生成大量小js文件。

然而,如果我编译一个没有太多依赖关系的单个文件,我得到的时间约为5秒(我已经分别为每个* .ts文件执行tsc来测量这个,有几个异常值需要更多超过10秒,但大多数文件编译得很快,因为它们接近依赖关系层次的底部)。 所以我的第一个想法是创建一个系统:

  1. 监视新的和修改过的* .ts文件
  2. 对修改后的文件执行tsc foo.ts
  3. 连接所有* .js文件,保留依赖顺序
  4. 您可以通过比较ls -ltc --full-time $(find ts -type f -name '*.ts')每秒的结果或更高级的inotify来实现第一步。 第3步并不难,因为tsc保留了js文件中的///引用注释,因此您可以搜索它们并执行简单的O(n)拓扑排序。

    但是,我认为我们可以通过使用tsc -d选项来创建声明文件来改进第二步。在第二步中,tsc不仅会创建foo.js,还会调查和编译foo.ts的所有依赖项,这是浪费时间。 OTOH如果foo.ts只引用了* .d.ts文件(反过来它们没有依赖关系,或者至少数量非常有限),重新编译foo.ts的过程可能会更快。

    为了使其工作,您必须找到并替换所有///引用,以便它们指向bar.d.ts,而不是bar.ts。

    find -name '*.ts' | 
    xargs sed -i -r 's/(\/\/\/<reference path=".*([^d]|[^\.]d)).ts"\/>/\1.d.ts"\/>/g'
    

    应该进行必要的改变。

    您还需要首次生成所有* .d.ts文件。 它有点像鸡和蛋的问题,因为你需要这些文件来执行任何编译。 好消息是,如果按引用的拓扑顺序编译文件,这应该有效。

    所以我们需要构建一个拓扑排序的文件列表。如果您有边缘列表,则有tsort程序执行此任务。 我可以使用以下grep

    找到所有依赖项
    grep --include=*.ts --exclude=*.d.ts --exclude-dir=.svn -o '///<reference path.*"/>' -R .
    

    唯一的问题是输出包含逐字引用,例如:

    ./entities/school_classes.ts:///<reference path="../common/app_backbone.d.ts"/>
    

    这意味着我们必须解析某些规范形式的相对路径。还有一个细节是,我们实际上依赖* .ts而不是* .d.ts来进行排序。 这个简单的parse.sh脚本负责:

    #!/bin/bash
    here=`pwd`
    while read line
    do
       a=${line/:*/}
       t=${line/\"\/>/}
       b=${t/*\"/}
       c=$(cd `dirname $a`;cd `dirname $b`;pwd);d=$(cd `dirname $a`;basename $b);
       B="$c/$d"
       B=${B/$here/.}
       B=${B/.d.ts/.ts}
       echo "$a $B"
    done
    

    将所有内容与tsort放在一起以正确的顺序生成文件列表:

    grep --include=*.ts --exclude=*.d.ts --exclude-dir=.svn -o '///<reference path.*"/>' -R . |  
    ./parse.sh | 
    tsort |
    xargs -n1 tsc -d
    

    现在,这可能会失败,因为如果项目从未以这种方式编译,那么它可能没有足够精确地定义依赖关系来避免问题。此外,如果您在整个应用程序(var myApp;)中使用某些全局变量(如myApp.doSomething()),则可能需要在* .d.ts文件中声明它并引用它。现在,您可能认为这会创建一个循环依赖(app需要模块x,而模块x需要app),但请记住我们现在只依赖于* .d.ts文件。所以现在没有循环(这有点类似于它在C或C ++中的工作方式,其中一个只依赖于头文件)。

    修复所有缺失的引用并编译所有* .d.ts和* .ts文件。您可以开始观察更改并仅重新编译已更改的文件。 但要注意,如果您更改文件foo.ts中的某些内容,您可能还会重新编译需要foo.ts的文件。不是因为有必要更新它们 - 实际上它们在重新编译期间根本不应该改变。相反,这需要验证 - foo.d.ts的所有用户都应检查foo的新接口是否与它们使用的方式兼容! 因此,您可能希望在foo.d.ts更改时重新编译foo.d.ts的所有用户。这可能会更加棘手和耗时,但很少发生(只有当你改变foo的形状时)。 另一种选择是在这种情况下简单地重建所有内容(按拓扑顺序)。

    我正在实施这种方法,所以我会在完成(或失败)后更新我的答案。

    <强>更新 所以,我设法使用tsort和gnu make来实现所有这些,通过依赖解析使我的生活变得更轻松。 问题是它实际上比原始tsc --out app.js app.ts慢。 这背后的原因是0.9.1.1编译器执行单个编译会有很大的开销 - 即使对于像

    这样简单的文件也是如此
    class A{
    }
    

    time tsc test.ts收益超过3秒。 现在,如果你必须重新编译一个文件,这很好。 但是一旦你意识到你必须重新编译依赖于它的所有文件(主要是执行类型检查),然后重新编译依赖它们的文件等,你需要执行5到10次这样的编译。 因此,即使每个编译步骤非常快(3秒<25秒),总体而言 经验更糟糕(约50秒!)。

    本练习对我来说的主要好处是,我必须修复许多错误并缺少依赖项才能使其工作:)

答案 2 :(得分:0)

我也开始使用tsc --out的Makefile,但我现在使用requirejs tsc --watch --module amd

tsc --watch可能有更好的选择(它不是很快),但requirejs有一些好处,你可以使用import代替// <reference.../>(.d.ts除外)文件)和requirejs以后也可以捆绑和优化你的项目。