🔎 Search Terms
sourceMap emit slow, emitSourceMapsBeforeNode, emitSourceMapsAfterNode, source map performance native port, large object literal emit
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about source map emit performance
Measured on typescript@next (7.1.0-dev.20260902.1), so this is not something already fixed on main. It is also present in 7.0.2 at the same magnitude.
This is not a regression within 7.x. It is a difference between Corsa and TypeScript 6 on the same input.
For context, typescript-go#2265 reported source map work running even with maps disabled, and was closed by typescript-go#2872 ("Speed up source map emit with printer-local monotomic cache", merged 2026-02-23). That is a different case from this one, where maps are enabled. It is worth mentioning only because #2872 touched source map emit, and the versions measured below both include it.
tsc --version → 7.1.0-dev.20260902.1 (typescript@next); also checked on 7.0.2
tsc6 --version → 6.0.3
- Node v24.18.0
- macOS, arm64 (Apple Silicon)
--singleThreaded is passed to the native tsc runs to remove scheduling noise; the effect reproduces without it
The affected input is produced by graphql-code-generator:
@graphql-codegen/cli 6.1.2
@graphql-codegen/typed-document-node 6.1.8
@graphql-codegen/typescript-operations 5.0.8
@graphql-codegen/near-operation-file-preset 5.1.0
graphql 16.13.2
⏯ Playground Link
N/A. This is a wall-clock difference during emit, so it needs an actual tsc run over files on disk.
💻 Code
This is a generator script rather than a pasteable sample, for two reasons. The input that triggers the problem is a single ~38 KB line, which is not readable inline in an issue. And the symptom is emit time rather than a wrong output, so it has to be measured against real files instead of read off a snippet. The script is around 50 lines, has no dependencies, and writes both the sources and the tsconfig, so setup is one command.
// make-repro.mjs
import fs from "node:fs";
import path from "node:path";
const OUT = process.argv[2] ?? "/tmp/tsgo-repro";
const FILE_COUNT = Number(process.argv[3] ?? 300);
const TARGET_KB = Number(process.argv[4] ?? 36);
fs.rmSync(OUT, { recursive: true, force: true });
fs.mkdirSync(path.join(OUT, "src"), { recursive: true });
// A deeply nested object literal, emitted on a single line.
const node = (depth, i) =>
depth === 0
? `{"kind":"Name","value":"leaf${i}"}`
: `{"kind":"Field","name":{"kind":"Name","value":"f${i}"},"selectionSet":{"kind":"SelectionSet","selections":[` +
`${node(depth - 1, i * 2)},${node(depth - 1, i * 2 + 1)}]}}`;
const buildLiteral = (targetBytes) => {
const parts = [];
let size = 0;
for (let i = 0; size < targetBytes; i++) {
const s = node(4, i);
parts.push(s);
size += s.length + 1;
}
return `{"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","selectionSet":{"kind":"SelectionSet","selections":[${parts.join(",")}]}}]}`;
};
const literal = buildLiteral(TARGET_KB * 1024);
for (let f = 0; f < FILE_COUNT; f++) {
fs.writeFileSync(
path.join(OUT, "src", `doc${String(f).padStart(4, "0")}.ts`),
`export const doc${f} = ${literal};\n`
);
}
fs.writeFileSync(
path.join(OUT, "tsconfig.json"),
JSON.stringify(
{
compilerOptions: {
target: "ES2022", module: "esnext", moduleResolution: "bundler",
outDir: "./dist", rootDir: "./src", strict: true,
declaration: true, declarationMap: true, sourceMap: true,
isolatedModules: true, skipLibCheck: true, noEmit: false,
},
include: ["src/**/*.ts"],
},
null, 2
) + "\n"
);
const bytes = fs.statSync(path.join(OUT, "src", "doc0000.ts")).size;
console.log(`${OUT}: ${FILE_COUNT} files / ${(bytes / 1024).toFixed(1)} KB each / one ${literal.length}-char line`);
node make-repro.mjs /tmp/tsgo-repro
cd /tmp/tsgo-repro
tsc --project tsconfig.json --singleThreaded
tsc --project tsconfig.json --singleThreaded --sourceMap false --declarationMap false
The file count is the second argument and defaults to 300 so the difference is unmistakable. The cost turns out to be per file (see below), so a smaller count shows the same effect at proportionally smaller magnitude — node make-repro.mjs /tmp/tsgo-repro 10 is enough to see it without waiting half a minute.
🙁 Actual behavior
All numbers below come from the generator above at its default 300 files, so they are reproducible as-is. Every compiler gets the identical tsconfig and identical input. Wall clock, warmed: one discarded run, then the faster of two.
| compiler |
sourceMap |
declarationMap |
wall clock |
emitted files |
tsc 7.1.0-dev.20260902.1 |
on |
on |
33.39 s |
1200 |
tsc 7.1.0-dev.20260902.1 |
off |
on |
7.21 s |
900 |
tsc 7.1.0-dev.20260902.1 |
off |
off |
7.30 s |
600 |
tsc6 6.0.3 |
on |
on |
4.08 s |
1200 |
tsc6 6.0.3 |
off |
on |
3.70 s |
900 |
tsc6 6.0.3 |
off |
off |
3.62 s |
600 |
Turning on sourceMap costs +26.2 s on the nightly and +0.4 s on 6.0.3. declarationMap is free on both implementations, within noise, so the whole difference comes from .js.map. Emitted file counts match between compilers for the same options, so neither is producing more output. Peak RSS was comparable.
The scaling itself is fine — the cost is linear in the amount of input on both implementations. What differs is the constant. On the nightly the sourceMap cost per file stays at roughly 90 ms across every size we tried:
| files |
sourceMap on |
off |
delta |
per file |
| 1 |
0.17 s |
0.08 s |
0.09 s |
90 ms |
| 10 |
1.19 s |
0.29 s |
0.90 s |
90 ms |
| 50 |
5.67 s |
1.21 s |
4.46 s |
89 ms |
| 300 |
33.71 s |
7.09 s |
26.62 s |
89 ms |
On 6.0.3 the same per-file cost is about 1.3 ms (0.38 s over 300 files), so Corsa spends roughly 70x more per file on the same source maps.
Each file holds exactly one 38 KB line. Since every file in the repro is the same size, we have not separated "per file" from "per byte of input" — both grow together here.
We originally hit this on a codebase of 1,209 comparable modules, where the same build went from 5 s to 88 s.
Narrowing down the trigger — one module from that codebase, variants with pieces removed, 300 copies of each, maps on, 7.0.2. Removing the type assertions, the large exported type declarations, or the cross-file import type each changed nothing (21-22 s, same as as-is). Removing the object literal value and keeping only the type declarations dropped it to 1 s.
The trigger is the large inline object literal value by itself.
🙂 Expected behavior
We expected the per-file cost of sourceMap to be in the same ballpark as 6.0.3 for the same input, rather than roughly 70x higher. Scaling is not the issue — both implementations are linear in input size, and it is the constant that is far apart.
Additional information about the issue
First, thanks for the work on the native port. On our own project, emit without source maps is faster on Corsa than on 6.0.3, which is why we moved our build over to it. This report is about the one case where that did not hold for us.
On the input shape: it is machine-generated rather than hand-written. @graphql-codegen/typed-document-node emits each GraphQL operation's AST as a single-line object literal.
export const UpdateItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateItem"}, /* … */ }]} as unknown as DocumentNode<…>;
In the codebase mentioned above that is one 37,035-character line per file. A single line that long is unusual input, and we are not arguing it is reasonable — the generator only mimics it so the repro needs no GraphQL dependency.
We are reporting the difference between the two implementations on the same input rather than the absolute cost. It is an odd input, but if it got faster we could keep emitting with tsc alone instead of pulling a bundler into the pipeline just for this step, which would be a real help on our end.
🔎 Search Terms
sourceMap emit slow, emitSourceMapsBeforeNode, emitSourceMapsAfterNode, source map performance native port, large object literal emit
🕗 Version & Regression Information
Measured on
typescript@next(7.1.0-dev.20260902.1), so this is not something already fixed on main. It is also present in 7.0.2 at the same magnitude.This is not a regression within 7.x. It is a difference between Corsa and TypeScript 6 on the same input.
For context, typescript-go#2265 reported source map work running even with maps disabled, and was closed by typescript-go#2872 ("Speed up source map emit with printer-local monotomic cache", merged 2026-02-23). That is a different case from this one, where maps are enabled. It is worth mentioning only because #2872 touched source map emit, and the versions measured below both include it.
tsc --version→ 7.1.0-dev.20260902.1 (typescript@next); also checked on 7.0.2tsc6 --version→ 6.0.3--singleThreadedis passed to the nativetscruns to remove scheduling noise; the effect reproduces without itThe affected input is produced by graphql-code-generator:
@graphql-codegen/cli6.1.2@graphql-codegen/typed-document-node6.1.8@graphql-codegen/typescript-operations5.0.8@graphql-codegen/near-operation-file-preset5.1.0graphql16.13.2⏯ Playground Link
N/A. This is a wall-clock difference during emit, so it needs an actual
tscrun over files on disk.💻 Code
This is a generator script rather than a pasteable sample, for two reasons. The input that triggers the problem is a single ~38 KB line, which is not readable inline in an issue. And the symptom is emit time rather than a wrong output, so it has to be measured against real files instead of read off a snippet. The script is around 50 lines, has no dependencies, and writes both the sources and the tsconfig, so setup is one command.
The file count is the second argument and defaults to 300 so the difference is unmistakable. The cost turns out to be per file (see below), so a smaller count shows the same effect at proportionally smaller magnitude —
node make-repro.mjs /tmp/tsgo-repro 10is enough to see it without waiting half a minute.🙁 Actual behavior
All numbers below come from the generator above at its default 300 files, so they are reproducible as-is. Every compiler gets the identical tsconfig and identical input. Wall clock, warmed: one discarded run, then the faster of two.
sourceMapdeclarationMaptsc7.1.0-dev.20260902.1tsc7.1.0-dev.20260902.1tsc7.1.0-dev.20260902.1tsc66.0.3tsc66.0.3tsc66.0.3Turning on
sourceMapcosts +26.2 s on the nightly and +0.4 s on 6.0.3.declarationMapis free on both implementations, within noise, so the whole difference comes from.js.map. Emitted file counts match between compilers for the same options, so neither is producing more output. Peak RSS was comparable.The scaling itself is fine — the cost is linear in the amount of input on both implementations. What differs is the constant. On the nightly the
sourceMapcost per file stays at roughly 90 ms across every size we tried:sourceMaponOn 6.0.3 the same per-file cost is about 1.3 ms (0.38 s over 300 files), so Corsa spends roughly 70x more per file on the same source maps.
Each file holds exactly one 38 KB line. Since every file in the repro is the same size, we have not separated "per file" from "per byte of input" — both grow together here.
We originally hit this on a codebase of 1,209 comparable modules, where the same build went from 5 s to 88 s.
Narrowing down the trigger — one module from that codebase, variants with pieces removed, 300 copies of each, maps on, 7.0.2. Removing the type assertions, the large exported type declarations, or the cross-file
import typeeach changed nothing (21-22 s, same as as-is). Removing the object literal value and keeping only the type declarations dropped it to 1 s.The trigger is the large inline object literal value by itself.
🙂 Expected behavior
We expected the per-file cost of
sourceMapto be in the same ballpark as 6.0.3 for the same input, rather than roughly 70x higher. Scaling is not the issue — both implementations are linear in input size, and it is the constant that is far apart.Additional information about the issue
First, thanks for the work on the native port. On our own project, emit without source maps is faster on Corsa than on 6.0.3, which is why we moved our build over to it. This report is about the one case where that did not hold for us.
On the input shape: it is machine-generated rather than hand-written.
@graphql-codegen/typed-document-nodeemits each GraphQL operation's AST as a single-line object literal.In the codebase mentioned above that is one 37,035-character line per file. A single line that long is unusual input, and we are not arguing it is reasonable — the generator only mimics it so the repro needs no GraphQL dependency.
We are reporting the difference between the two implementations on the same input rather than the absolute cost. It is an odd input, but if it got faster we could keep emitting with
tscalone instead of pulling a bundler into the pipeline just for this step, which would be a real help on our end.