Remove need for babel or swc plugin on client preset for reducing bundle size/code splitting
Is your feature request related to a problem? Please describe.
Yes, it's related to a problem. In a site with a large number of client side queries, we get a very large graphq.ts bundle, just to support types. These types should not add ANY weight to the site.
Describe the solution you'd like
I'd like for the generated graphq.ts file to simply use a different format, so it doesn't generate this giant blob.
Describe alternatives you've considered
I can manually edit this thing so that it generates very minimal code like so:
import * as types from "./graphql";
type QueryMap = {
"query GetUser($id: ID!) { user(id: $id) { name } }": typeof types.GetUserDocument,
"query GetUsername($id: ID!) { user(id: $id) { username } }": typeof types.GetUsernameDocument,
};
// Overloaded function signatures
export function graphql<TSource extends keyof QueryMap>(source: TSource): QueryMap[TSource];
export function graphql(source: string): unknown;
// Function implementation
export function graphql(source: string) {
return source;
}
The effort to do this was simple - I used a multi-cursor selection to grab the types, and just pasted them as the return type for the function interfaces, then again for the union type in the graphql function implementation. Easy peasy - should be similarly easy to implement in the code gen.
Is your feature request related to a problem? Please describe.
Yes, it's related to the bloat you get from including 2 copies of largish strings, which can't be code-split with tree shaking.
The solution doesn't present any problem, but the heuristics are different. In this code, it simply passes the string defined by the developer from their graphql file, back out. I see very little downside from doing this, especially if your build pipeline is set up to always regenerate the types. But it is different. In the current implementation, the graphql function returns the result of a lookup in a hash table. but the effects should generally be the same - the string you get back from that hash tables, is the same string you passed the function. In the new implementation, it just isn't in the bundle twice, and isn't in the bundle in a way that can't be tree shaken. I see mostly upside, and very little downside (or no downside) here.
AND - I no longer need to use a babel or SWC plugin, which I can't get to work anyway... (And you guys don't need to maintain them any more, or deal with tickets like these...)
It's pure gravy.
Update:
The original example I provided didn't quite do the same thing as the generated code currently does - this one should.
By doing this globally referenced fragments could no longer be used within the GraphQL operation strings.
Hmm, I didn't consider fragments. Can you show me an example? I'd like to see if I can work out a pattern that would support that.
@CaptainN https://github.com/dotansimha/graphql-code-generator/blob/c720b1b86211ec6f6248b4e54a2813f56e3904b1/examples/react/apollo-client/src/App.tsx#L8-L18
I added a global fragment to my playground, and ran the generator - it doesn't look like it does anything all that different from including any other query. It doesn't seem like anything else uses the documents object, which is the thing I want to get rid of. I think this would still work as expected:
import * as types from "./graphql";
type QueryMap = {
"fragment UserFields on User { name }": typeof types.UserFieldsFragmentDoc
"query GetUser($id: ID!) { user(id: $id) { ...UserFields } }": typeof types.GetUserDocument,
"query GetUsername($id: ID!) { user(id: $id) { username } }": typeof types.GetUsernameDocument,
};
// Overloaded function signatures
export function graphql<TSource extends keyof QueryMap>(source: TSource): QueryMap[TSource];
export function graphql(source: string): unknown;
// Function implementation
export function graphql(source: string) {
return source;
}
Am I missing some other type of output?
Yeah, if the graphql function only returns the parameter passed to it:
export function graphql(source: string) {
return source;
}
That string does not contain the fragment definition, thus when the string is sent to the server, the GraphQL execution will fail.
In addition to that, features like persisted documents will no longer work without the lookup map/babel/swc plugin.
Got it. So you still need some kind of runtime lookup system, at least for fragments.
it's just that the map is huge, and the babel/swc plugins either don't work at all (swc) in nextjs, or don't seem to do much even if you get it to work (babel) in nextjs
(And SWC produces smaller chunks - so you actually overall lose budget if you use babel vs swc in next.js - even if I can get the graphql map size down to essentially 0)
The Babel plugin works perfectly for us. SWC has known issues.
We are happy for suggestions on alternative approaches, but right now I am afraid the lookup map (by default) is the only solution that does not force people to use the plugins.
Heck, I'd be happy with a solution that does what I've proposed, maybe hidden behind a setting in codegen.ts, even if it breaks global Fragments.
To me, this isn't terrible:
import { graphql } from "data/types/gql";
import ReportFieldsFragment from "data/fragments/ReportFieldFragment";
export default graphql(`
${ReportFieldsFragment}
query reports($order: [Core_EditionSortInput!]) {
core_reports(order: $order) {
...ReportFields
}
}
`);
I don't use persisted queries (without APQ anyway), so that's fine.
We could consider passing the fragments as an array parameter to the graphql function, then the graphql function could just concatenate the strings. 🤔
export default graphql(`
query reports($order: [Core_EditionSortInput!]) {
core_reports(order: $order) {
...ReportFields
}
}
`, [ReportFieldsFragment]);
But, this could then cause issues where the same fragment is printed twice in the operation sent to the server because it was referenced within two or more fragments referenced by a query/mutation/subscription operation. That would open a whole new deduplication rabbit-hole.
You could dedupe that array pretty easily. What I'm wondering is if there is some pattern on the other side (in gql.ts) we could use to insert the same (deduped) array, without too much overhead. Even something like a hybrid solution, where global fragments are defined the old way, and queries are defined the new way. I'll play with it more later.
Had to teach some karate classes, I'm back. I actually think your array solution would work nicely. There shouldn't be deduplication issues - if the Fragment is in the array once, it would only be included in the query package once. Should be pretty straight forward. Similar to the way I did it, without the use of template strings.
Function Implementation:
export function graphql(source: string, fragments: string[] = null) {
if (Array.isArray(fragments) && fragments.length > 0) {
return [...fragments, source].join("\n");
} else {
return source;
}
}
@CaptainN this solution won't work if a fragment is referenced by multiple fragments. You will end up with duplicated fragment definitions in the document string, which then will be rejected by the GraphQL server.
const A = graphql(`
fragment A on Query {
a
}
`)
const B = graphql(`
fragment B on Query {
...A
}
`, [A])
const Query = graphql(`
query {
...A
...B
}
`, [A, B])
Also, you will get no typescript compiler/codegen error if a fragment was not passed, but is required as it is used within that operation document.
Ah, thats interesting. We'd probably need to complicate the API to solve for that - like adding an additional method for registering fragments. But that's getting complicated:
const A = graphql.fragment(`
fragment A on Query {
a
}
`)
const B = graphql.fragment(`
fragment B on Query {
...A
}
`, [A])
const Query = graphql(`
query {
...A
...B
}
`, [A, B])
// implementation (ignoring typescript validation for the moment)
const fragStore = [];
function getFragmentsFromDeps(deps: string[]) {
return fragStore
.filter(frag => deps.includes(frag.fragAndDeps))
.map(frag => frag.fragment)
}
export function graphql(source: string, fragments: string[] = null) {
if (Array.isArray(fragments) && fragments.length > 0) {
const frags = getFragmentsFromDeps(fragments);
// dedupe frags, and combine with source
return [...new Set(frags), source].join("\n");
} else {
return source;
}
}
graphql.fragments = (fragment: string, deps: string[] = null) => {
const fragAndDeps = (Array.isArray(deps))
? [...new Set(getFragmentsFromDeps(deps)), fragment].join("\n")
: fragment;
fragStore.push({ fragment, deps, fragAndDeps });
return fragAndDeps; // I assume this needs to return the aggregated string for type gen to work
}
I didn't test any of that (there may be some cases where we need to recurse on the deps array - I'm not sure, would need to write some unit tests), but the idea would be to store enough info about the registered fragment, that we can reconstruct the correct query package later.
I don't know what impact that would have on type generation side of things, but on the runtime this would work, because the package resolver would make sure to include all of the necessary linked fragments in the correct order, as long as they properly specified in the deps arrays.
It should be possible to support even trickier cases like:
const A = graphql(`
fragment A on Query {
a
}
`)
const B = graphql(`
fragment B on Query {
...A
}
`, [A])
const Query = graphql(`
query {
...B
}
`, [B])
As far as typescript errors - yeah, that's an issue. We'd need some way to validate that, maybe an eslint rule? Again, adding more complexity...
For my project, I wrote a rollup plugin based on babel plugin that replaces the source code of graphql queries with generated code. This way tree-shaking graphql.ts is completely eliminated. It is also important to enable enumsAsTypes: true in the code generator settings
based on @ucw's solution, I created a vite + babel version here https://gist.github.com/sep2/094b71ca73fab3a20a43cef9cfb4c854
I created a vite-plugin that transforms using Typescript. It seems to be working fine. https://gist.github.com/Yama-Tomo/c51f87511e1e3cf101f2f5eb02cfb591
Has anyone got this working with recent NextJS? (with/without turbopack)? Going to try to get the Babel plugin working but curious on others' experiences.
I have tried and failed to get this to work with Next.js and Turbopack. I've tried using Babel also. This aspect of the project seems to have had little to no progress for years now so
I've used https://github.com/0no-co/gql.tada with a Next.js project as part of https://github.com/bigcommerce/catalyst and I've had no issues with it, so I may pivot to that approach when I find the time.
The generated output files when using this project are far too large for frontend usage.
On Wed, 19 Mar 2025 at 23:09, Rik Brown @.***> wrote:
Has anyone got this working with recent NextJS? (with/without turbopack)? Going to try to get the Babel plugin working but curious on others' experiences.
— Reply to this email directly, view it on GitHub https://github.com/dotansimha/graphql-code-generator/issues/9988#issuecomment-2738449299, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAMPFNKIODYG5BU6VI6X6JT2VH2KLAVCNFSM6AAAAABI3LFXY2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOMZYGQ2DSMRZHE . You are receiving this because you are subscribed to this thread.Message ID: @.***> [image: rik-iso]rik-iso left a comment (dotansimha/graphql-code-generator#9988) https://github.com/dotansimha/graphql-code-generator/issues/9988#issuecomment-2738449299
Has anyone got this working with recent NextJS? (with/without turbopack)? Going to try to get the Babel plugin working but curious on others' experiences.
— Reply to this email directly, view it on GitHub https://github.com/dotansimha/graphql-code-generator/issues/9988#issuecomment-2738449299, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAMPFNKIODYG5BU6VI6X6JT2VH2KLAVCNFSM6AAAAABI3LFXY2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOMZYGQ2DSMRZHE . You are receiving this because you are subscribed to this thread.Message ID: @.***>
@karlito-who-else gql.tada is really interesting. Could you share how many graphql queries in the project are using it and how about the typescript performance?
Funny you mention gql.tada, I saw it on Reddit yesterday after anger-searching for alternatives to this package after having these issues. It indeed seems to be really nice, seems to work very well from some initial experiments. I’ll give it a shot on a new project but don’t have bandwidth to migrate our existing code base yet.
Will something like this work? The idea is to keep the lookup object, but instead of storing the whole query string and blowing up the bundles, it uses a short hash. No plugin needed, which would be great for those of us using Vite and not wanting to incorporate babel.
type QueryMap = {
"query GetUser($id: ID!) { user(id: $id) { name } }": typeof types.GetUserDocument,
"query GetUsername($id: ID!) { user(id: $id) { username } }": typeof types.GetUsernameDocument,
};
// hash query strings into a short hash
const documentsMap = {
"b7d8ec4": types.GetUserDocument,
"a82731a": types.GetUsernameDocument
};
// Overloaded function signatures
export function graphql<TSource extends keyof QueryMap>(source: TSource): QueryMap[TSource];
export function graphql(source: string): unknown;
// Function implementation
export function graphql(source: string) {
const hash = someShortHashFunction(source);
return (documents as any)[hash] ?? {};
}
Hi all 👋 Great to see the ideas and discussions happening here, we have proposed a few options: https://github.com/dotansimha/graphql-code-generator/issues/10379
Please leave feedback/thoughts and if there are other ideas, feel free to propose!