Out of memory generating types with `near-operation-file` + `typescript-operations`

Author: jakubwawrzyczekCreated Sep 8, 2026Updated Sep 16, 2026

Which packages are impacted by your issue?

@graphql-codegen/visitor-plugin-common

Describe the bug

We tried to upgrade visitor-plugin-common from 5.8.0 to 7.2.5. After the upgrade, codegen runs out of memory and never finishes.

Our config has four outputs in which three of them still work. The one that breaks is using near-operation-file and typescript-operations.

Increasing memory does not help:

  • 4 GB -> crashes after 75s
  • 8 GB -> crashes after 112s

It used all 8 GB, so it is not a matter of raising the limit. The garbage collector was busy about 90% of the time and could not free anything, so something is being held onto.

I looked into it and found two things

I did this investigation with an AI agent doing the profiling and the patching. The measurements below are real, taken from heap snapshots and allocation profiles of our own repo, and I verified each change by re-running codegen. But I do not know this codebase well myself, so if you ask follow-up questions I may need a little time to come back with a proper answer.

1. The cache key is the entire list of field paths

The cache uses a label to look things up, and that label is every field name in the selection set glued into one string. For our schema a single label is 168 MB, and the labels are kept until the run ends, so memory fills up with labels.

In transformSelectionSet, selection-set-to-object.ts#L1110C1-L1134C68:

typescript
const fieldSelections = [...getFieldNames({ selections, loadedFragments })].sort();
const cacheHashKey = `${fieldSelections.join(',')} @ ${possibleTypes.join('|')}`;
objMap.set(cacheHashKey, [result.mergedTypeString, fieldName]);

The key is every field path in the selection set glued together with commas, and it stays in processor.typeCache until the run ends. In our project the three biggest keys are 168 MB, 166 MB and 154 MB. They are strings.

We changed the key to a hash and took heap snapshots before and after, at the same memory limit:

strings over 16KB: 517 MB in 261 strings -> 30 MB in 298 strings

So the key really was the problem for those 517 MB. The hash keeps the cache working:

typescript
const h = createHash('sha1');
for (const f of fieldSelections) { h.update(f); h.update(','); }
h.update(' @ ');
for (const p of possibleTypes) { h.update(p); h.update('|'); }
const cacheHashKey = h.digest('base64');

2. getFieldNames walks the same fragments over and over

If a fragment is used in ten places, its subtree gets walked ten times instead of once and remembered. With fragments nested inside fragments, that multiplies.

In getFieldNames, utils.ts#L689-L699

typescript
case Kind.FRAGMENT_SPREAD: {
    getFieldNames({
        selections: loadedFragments
            .filter(def => def.name === selection.name.value)
            .flatMap(s => s.node.selectionSet.selections),
        fieldNames, parentName, loadedFragments,
    });

Every time a fragment is used, its whole subtree is walked again, and loadedFragments.filter() scans all fragments once per use. This is the same M^N problem described in #752. A memory profile blames 91.7% of allocations on Set.prototype.add inside getFieldNames, calling itself dozens of levels deep.

The field paths inside a fragment do not depend on where the fragment is used, so they can be computed once per fragment and reused. We tried that and CPU time went from 98s to 60s, with exactly the same generated files.

What we could not figure out

With both changes applied it still runs out of memory. At that point the memory is not a few huge strings any more, it is about 3.2 million small strings. We could not tell what holds them, so we are reporting what we measured instead of guessing.

Things we tried that changed nothing: deduplicating fragment definitions, inlineFragmentTypes: 'inline' instead of 'combine', and skipping buildParentFieldName when extractAllFieldsToTypes is off. The fixes from #752 and #10895 are both already in the versions we use.

Setup

typescript
{
  preset: 'near-operation-file',
  plugins: ['typescript-operations'],
  config: {
    inlineFragmentTypes: 'combine',
    declarationKind: 'interface',
    nonOptionalTypename: true,
    exportFragmentSpreadSubTypes: true,
    immutableTypes: true,
  },
}

It is a React Native app with a lot of deeply nested fragments that are reused in many places. The schema is about 26 MB as an AST.

This is not about a community plugin. Both problems are in core visitor-plugin-common. near-operation-file-preset is only part of the config needed to see it.

We cannot share a public reproduction because the project is a private repo, but we are happy to run any patch or alpha build and report the numbers back.

Your Example Website or App

Private repo, cannot share. Happy to test any patch or alpha build and report numbers back.

Steps to Reproduce the Bug or Issue

  1. A project using near-operation-file preset with typescript-operations, with deeply nested fragments reused across many operations. Ours has a ~26 MB schema AST.
  2. With [email protected] (which pulls [email protected]), run codegen. It completes normally.
  3. Upgrade to [email protected] (which pulls [email protected]), keeping cli on 5.x. Run codegen again.
  4. The output using near-operation-file + typescript-operations runs out of memory and never finishes. The other outputs in the same config still complete.

Expected behavior

Codegen generates the types without running out of memory.

Screenshots or Videos

No response

Platform

Codegen Config File

{ schema: 'src/schema/schema.graphql', generates: { 'src/': { documents: ['src/**/*.tsx'], plugins: ['typescript-operations'], preset: 'near-operation-file', presetConfig: { baseTypesPath: 'graphql/globalTypes.ts', extension: '.ts', folder: 'graphql', }, config: { inlineFragmentTypes: 'combine', declarationKind: 'interface', nonOptionalTypename: true, exportFragmentSpreadSubTypes: true, immutableTypes: true, maybeValue: 'T | null', omitOperationSuffix: true, arrayInputCoercion: false, }, }, }, }

Additional context

No response

Source: dotansimha/graphql-code-generator