Hacker News new | past | comments | ask | show | jobs | submit login
A principled approach to GraphQL query cost analysis (medium.com)
46 points by davisjam314 15 days ago | hide | past | favorite | 11 comments

I'm finding GraphQL very difficult to manage for a team of developers that is learning it as they go. Generally it seems that if we do what GraphQL wants us to do with the ability of deeply nested queries, the complexity increases so much that we have the dual pain of weird corner case performance bugs and highly complex QA scenarios. And if we start tearing apart the queries to force the frontend to make more numerous smaller queries, then we lose much of the benefit. I'm suspecting that the team is too focused on a false choice of a spectrum and that there's an entirely different philosophy we should be following to get the best of both worlds (predictability and flexibility) but we haven't been able to put our finger on it yet.

In my experience, GraphQL tempts one to ask for more information than necessary at the moment. That might be useful in just another click or two, but then again, might not.

As you say, smaller, simpler queries are more performant. They also tend to be more relevant. It's kinda like the argument for 2 dozen eggs at $1.60 instead of a dozen eggs for $1. Buying 2 dozen at a time optimizes the cost per egg. But if you only ever eat 10 eggs before you throw the rest away, cost per egg is not the most important factor.

I prefer to pull data on demand in smaller chunks. At which point, GraphQL makes less sense for my use case. But I think it's cool that somebody thought it up and I'm sure it's very helpful in lots of situations.

Fulcro in the CLJS world solved this by generating the graph query based on what's currently in the component tree, if you take react components out of your view your query automatically adjusts to remove the components requirements from the graph call

I imagine most js frameworks are greedy because it's extra work to be lean here

The question I haven't really been able to get answered from GraphQL proponents is how is this any different than allowing frontend dev's to code a Node endpoint for the given task? It seems easier and more restrictive (principle of least privilege) to do so and you can ensure performance/SLI's without complex solutions only a subject matter expert on GraphQL understands. Sure you have a bunch of endpoints, but you would otherwise have a bunch of queries that have to be maintained as well. At the very least you don't have to serialise query logic to the client.

If you have a general endpoint that doesn't have performance guarantees and you need to distribute widely seems to make sense (e.g. a product you want to sell to other companies with a general query interface). I'm sure there is good use cases; but I'm not sure a general website for a company for example is one. I'm curious what the general advantage is as a "default option" for frontend dev's.

I’ve worked on projects which had page/task specific endpoints, projects that were strict about keeping endpoints entity specific (traditional REST), and projects that used GraphQL.

I tend to use GraphQL as a default for anything moderately complex or likely to become complex because of the development flow it enables. But this flow also requires apollo; GraphQL is most appealing when you have a good frontend client like apollo.

Here’s the flow: I’ll get a spec for a feature and figure out what data it will need. Specifically, I’ll figure out what data each component in my frontend will need (I often use React). Then I’ll write a naive graphql query in order for each component to fetch the data it needs.

That might sound like a lot of querying and something you could just as easily implement with REST, which would seemingly defeat the purpose of using GraphQL. But apollo enables two things that offsets this, which it can do because of the way in which you can ask for multiple pieces of data at once and the type information GraphQL sends back. If a bunch of queries are made in rapid succession, you can configure apollo to combine those queries into a single network request. It will also cache entities by id and type name and parameters. This means that all of the little queries you write will typically pull from the cache rather than the server, and will be updated automatically if another query or mutation elsewhere in the app alters that entity.

Writing small, entity based queries is typically much faster to implement than creating a new page specific endpoint for every new page/task, as you can reuse and combine queries/types that you’ve already defined. And because of the optimizations apollo makes when deciding whether to query, you often don’t get a performance penalty from it.

At this stage, I’ll evaluate whether or not the performance is acceptable. Normally it is, but sometimes it’s not. When it’s not, I’ll move those little queries that were happening in the components out, and have the component get the data from higher up in the component hierarchy. The component logic stays the same, so the move is cheap in terms of dev time.

I‘ll then write more performant, page/task specific queries (top level graphql query types) that fetch a whole bunch of stuff at once and do so efficiently. If a frontend dev could write a task specific node endpoint, they could write a graphql resolver for a task specific top level query the same way.

That's also in my opinion the hardest part in the backend. In the frontend, the most difficult part has been to support dynamic fields at runtime. E.g. a webuser chooses which fields they want to see.

Sure, you can use the "include" directive in ".gql" files to subset the fields. But it's a pain to use, specially in nested queries.

That is an interesting use case: GraphQL API Management could help you during your testing cycle quickly identifying problematic queries

Your suspicions are probably right.

Care to share any specific problem you are currently facing?

The most recent discussions have had to do with how to handle queries that return different results depending on whether the user making the query is authenticated; whether to use the same resolver or to instead require the frontend to use a different "whateverPublic" query if they know the user isn't authenticated.

Another is what to do when subqueries yield errors. We're often faced with three choices there: whether to make the entire query return an error, or make the subquery return a friendly error, or make the subquery return null/empty.

Finally, we're suspecting that in most cases where it might first appear we need a dataloader, maybe we don't if we change how the frontend queries, since the "+1" part of the "N+1" can often make its own group query to whatever repository we're using.

If you aren't already, I strongly recommend using union types for query results with inline fragments in your queries themselves. This handles both your auth/unauth case as well as error handling. This pattern isn't discussed much in tutorials/docs, but has been absolutely essential for us.

Why you should measure the cost of your GraphQL queries, and how you should do it.

This is a summary of a research paper.

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact