Skip to content

Commit

Permalink
Update performance docs
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalLytek committed Jan 2, 2020
1 parent 1a9dd5e commit d6f6ad3
Showing 1 changed file with 39 additions and 16 deletions.
55 changes: 39 additions & 16 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,46 @@ While this enable easy and convenient development, it's sometimes a tradeoff in

To measure the overhead of the abstraction, a few demo examples were made to compare the usage of TypeGraphQL against the implementations using "bare metal" - raw `graphql-js` library. The benchmarks are located in a [folder on the GitHub repo](https://github.com/MichalLytek/type-graphql/tree/master/benchmarks).

The most demanding cases like returning an array of 10 000 nested objects shows that it's about 4 times slower. In real apps (e.g. with complex database queries) it's usually a much lower factor but still not negligible. That's why TypeGraphQL has some built-in performance optimization options.
The most demanding cases like returning an array of 25 000 nested objects showed that in some cases it might be about 5 times slower.

| | 25 000 array items | Deeply nested object |
| -------------------- | :----------------: | :------------------: |
| Standard TypeGraphQL | 1253.28 ms | 45.57 μs |
| `graphql-js` | 265.52 ms | 24.22 μs |

In real apps (e.g. with complex database queries) it's usually a much lower factor but still not negligible. That's why TypeGraphQL has some built-in performance optimization options.

## Optimizations

When we have a query that returns a huge amount of JSON-like data and we don't need any field-level access control, we can turn off the whole authorization and middlewares stack for selected field resolvers using a `{ simple: true }` decorator option, e.g.:
Promises in JS have a quite big performance overhead. In the same example of returning an array with 25 000 items, if we change the Object Type field resolvers to an asynchronous one that return a promise, the execution slows down by a half even in "raw" `graphql-js`.

| `graphql-js` | 25 000 array items |
| --------------- | :----------------: |
| sync resolvers | 265.52 ms |
| async resolvers | 512.61 ms |

TypeGraphQL tries to avoid the async execution path when it's possible, e.g. if the resolver class method is not using args and doesn't return a promise. So if you find a bottleneck in your app, try to investigate your resolvers and maybe remove some unnecessary async/await usage.

Also, using middlewares implicitly turns on the async execution path (for global middlewares the middlewares stack is created even for every implicit field resolver!), so be careful when using this feature if you care about the performance very much (and maybe then use the "simple resolvers" tweak described below).

The whole middleware stack will be soon redesigned with a performance in mind and with a new API that will also allow fine-grained scoping of global middlewares. Stay tuned!

## Further performance tweaks

When we have a query that returns a huge amount of JSON-like data and we don't need any field-level access control or other custom middlewares, we can turn off the whole authorization and middlewares stack for selected field resolver using a `{ simple: true }` decorator option, e.g.:

```typescript
@ObjectType()
class SampleObject {
@Field()
sampleField: string;

@Field(type => [SuperComplexObject], { simple: true })
superComplexData: SuperComplexObject[];
@Field({ simple: true })
publicFrequentlyQueriedField: SomeType;
}
```

This simple trick can speed up the execution up to 72%! The benchmarks show that using simple resolvers allows for as fast execution as with bare `graphql-js` - the measured overhead is only about ~20%, which is a much more reasonable value than 400%. Below you can see [the benchmarks results](https://github.com/MichalLytek/type-graphql/tree/master/benchmarks):

| | 10 000 array items | Deeply nested object |
| ---------------------------------- | :----------------: | :------------------: |
| Standard TypeGraphQL | 42.551s | 4.557s |
| `graphql-js` | 9.963s | 2.422s |
| TypeGraphQL with "simpleResolvers" | 11.841s | 3.086s |

Moreover, you can also apply this behavior for all the fields of the object type by using a `{ simpleResolvers: true }` decorator option, e.g.:
Moreover, we can also apply this behavior for all the fields of the object type by using a `{ simpleResolvers: true }` decorator option, e.g.:

```typescript
@ObjectType({ simpleResolvers: true })
Expand All @@ -51,8 +65,17 @@ class Post {
}
```

### Caution
This simple trick can speed up the execution up to 76%! The benchmarks show that using simple resolvers allows for as fast execution as with bare `graphql-js` - the measured overhead is only about ~13%, which is a much more reasonable value than 500%. Below you can see [the benchmarks results](https://github.com/MichalLytek/type-graphql/tree/master/benchmarks):

| | 25 000 array items |
| ----------------------------------------------------------------------------- | :----------------: |
| `graphql-js` | 265.52 ms |
| Standard TypeGraphQL | 310.36 ms |
| TypeGraphQL with a global middleware | 1253.28 ms |
| **TypeGraphQL with "simpleResolvers" applied <br> (and a global middleware)** | **299.61 ms** |

> This optimization **is not turned on by default** mostly because of the global middlewares and authorization feature.
This optimization **is not turned on by default** mostly because of the global middlewares and authorization feature. By using "simple resolvers" we are turning them off, so we have to be aware of the consequences - `@Authorized` guard on fields won't work for that fields so they will be publicly available, as well as global middlewares won't be executed for that fields, so we might lost, for example, performance metrics or access logs.
By using "simple resolvers" we are turning them off, so we have to be aware of the consequences - `@Authorized` guard on fields won't work for that fields so they will be publicly available, as well as global middlewares won't be executed for that fields, so we might lost, for example, performance metrics or access logs.

That's why we should **be really careful with using this feature**. The rule of thumb is to use "simple resolvers" only when it's really needed, like returning huge array of nested objects.
That's why we should **be really careful with using this tweak**. The rule of thumb is to use "simple resolvers" only when it's really needed, like returning huge array of nested objects.

0 comments on commit d6f6ad3

Please sign in to comment.