Frontend Development 9 min read

Understanding Incremental Compilation in TypeScript

TypeScript’s incremental compilation, introduced in version 3.4, speeds up build times by reusing previous build information stored in a tsBuildInfoFile, and this article explains the entry points, how old and current programs are created, change detection rules, and configuration of the buildInfo file.

ByteDance Web Infra
ByteDance Web Infra
ByteDance Web Infra
Understanding Incremental Compilation in TypeScript

1. Background

Modern front‑end projects are becoming increasingly complex, and as business requirements evolve the amount of code in a front‑end repository grows rapidly, leading to longer compilation times and a poorer developer experience. Optimising compilation speed is a crucial way to improve this experience.

Typical ways to speed up builds include upgrading hardware or optimising the compilation strategy. Common optimisation techniques are compilation caching and incremental compilation.

TypeScript introduced incremental compilation in version 3.4, which must be manually enabled in the configuration file.

2. Incremental Compilation in TypeScript

2.1 Incremental Compilation Entry

When the TypeScript compiler starts, it checks whether the incremental flag is set in the configuration. Enabling this flag activates the incremental compilation workflow.

if (ts.isWatchSet(configParseResult.options)) {
  if (reportWatchModeWithoutSysSupport(sys, reportDiagnostic))
    return;
  return createWatchOfConfigFile(sys, cb, reportDiagnostic, configParseResult, commandLineOptions, commandLine.watchOptions, extendedConfigCache);
}
// configure incremental flag to enable incremental compilation
else if (ts.isIncrementalCompilation(configParseResult.options)) {
  performIncrementalCompilation(sys, cb, reportDiagnostic, configParseResult);
}
// regular compilation
else {
  performCompilation(sys, cb, reportDiagnostic, configParseResult);
}

2.2 Incremental Compilation Process

Generate an old program from the buildInfo file.

During incremental compilation, the compiler reads the tsBuildInfoFile path, parses the buildInfo file, and creates an old program ( oldProgram ) based on it.

function createIncrementalProgram(_a) {
  var rootNames = _a.rootNames, options = _a.options, configFileParsingDiagnostics = _a.configFileParsingDiagnostics, projectReferences = _a.projectReferences, host = _a.host, createProgram = _a.createProgram;
  host = host || createIncrementalCompilerHost(options);
  createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram;
  // create old program
  var oldProgram = readBuilderProgram(options, host);
  return createProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences);
}

// create oldProgram based on tsBuildInfoFile
function readBuilderProgram(compilerOptions, host) {
  var buildInfoPath = ts.getTsBuildInfoEmitOutputFilePath(compilerOptions);
  if (!buildInfoPath) return undefined;
  var buildInfo;
  if (host.getBuildInfo) {
    buildInfo = host.getBuildInfo(buildInfoPath, compilerOptions.configFilePath);
    if (!buildInfo) return undefined;
  } else {
    var content = host.readFile(buildInfoPath);
    if (!content) return undefined;
    buildInfo = ts.getBuildInfo(content);
  }
  if (buildInfo.version !== ts.version) return undefined;
  if (!buildInfo.program) return undefined;
  return ts.createBuilderProgramUsingProgramBuildInfo(buildInfo.program, buildInfoPath, host);
}

Compare the old and new programs to find changed files.

The current compilation creates a currentProgram that references the oldProgram , allowing it to reuse previous compilation data.

function createIncrementalProgram(_a) {
  var rootNames = _a.rootNames, options = _a.options, configFileParsingDiagnostics = _a.configFileParsingDiagnostics, projectReferences = _a.projectReferences, host = _a.host, createProgram = _a.createProgram;
  host = host || createIncrementalCompilerHost(options);
  createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram;
  var oldProgram = readBuilderProgram(options, host);
  // create program for the current build
  return createProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences);
}

During the compilation, the compiler checks several rules to decide whether a file needs to be re‑compiled:

Whether previous build information exists.

Whether the previous build contains the file.

Whether the file’s content has changed (detected via a hash/version).

Whether the file format has changed.

Whether the import/reference relationships have changed.

state.fileInfos.forEach(function (info, sourceFilePath) {
  var oldInfo;
  var newReferences;
  if (!useOldState ||
      // rule 1: previous build exists
      // rule 2: previous build contains the file
      !(oldInfo = oldState.fileInfos.get(sourceFilePath)) ||
      // rule 3: content changed
      oldInfo.version !== info.version ||
      // rule 4: format changed
      oldInfo.impliedFormat !== info.impliedFormat ||
      // rule 5: reference changes
      !hasSameKeys(newReferences = referencedMap && referencedMap.getValues(sourceFilePath), oldReferencedMap && oldReferencedMap.getValues(sourceFilePath)) ||
      newReferences && ts.forEachKey(newReferences, function (path) { return !state.fileInfos.has(path) && oldState.fileInfos.has(path); })) {
    state.changedFilesSet.add(sourceFilePath);
  }
  // other codes
});

Changed files are added to changedFileSet , which is later consumed during the emit phase.

Update output artifacts based on changed files.

In the emit stage, the compiler iterates over changedFileSet , updating the compiled output for each changed file and also updating type definition files along the dependency chain.

function getNextAffectedFile(state, cancellationToken, computeHash, getCanonicalFileName, host) {
  var _a, _b;
  while (true) {
    var affectedFiles = state.affectedFiles;
    if (affectedFiles) {
      var seenAffectedFiles = state.seenAffectedFiles;
      var affectedFilesIndex = state.affectedFilesIndex;
      while (affectedFilesIndex < affectedFiles.length) {
        // other codes
        // update type definitions along the dependency chain
        handleDtsMayChangeOfAffectedFile(state, affectedFile, cancellationToken, computeHash, getCanonicalFileName, host);
        return affectedFile;
      }
      affectedFilesIndex++;
    }
    // other codes
  }
}

2.3 BuildInfo File Generation

After compilation, the compiler gathers all build information (the buildInfo) from the current program and writes it to the location specified by tsBuildInfoFile . If the option is not defined, the file is placed in the output directory.

function emitBuildInfo(bundle, buildInfoPath) {
  // other codes
  var buildInfo = { bundle: bundle, program: program, version: version };
  ts.writeFile(host, emitterDiagnostics, buildInfoPath, getBuildInfoText(buildInfo), false, undefined, { buildInfo: buildInfo });
}

3. How to Configure tsBuildInfoFile?

Each time tsc runs, it consumes the previous buildInfo and, after finishing, emits a new buildInfo for the next incremental compilation. The tsBuildInfoFile option lets you specify where this file is stored.

In most cases, the buildInfo is only used internally by TypeScript, so developers can ignore this setting.

In special scenarios—such as monorepos—different modules should have independent buildInfo locations to avoid accidental overwrites or cross‑module consumption.

TypeScriptfrontend developmentIncremental CompilationCompilation OptimizationBuildInfo
ByteDance Web Infra
Written by

ByteDance Web Infra

ByteDance Web Infra team, focused on delivering excellent technical solutions, building an open tech ecosystem, and advancing front-end technology within the company and the industry | The best way to predict the future is to create it

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.