FastAPI で レスポンスをgzip圧縮する

# tl;dr

FastAPI で responseをgzip圧縮で返却したい場合は
```python
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware

app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)
```
と記述する。

# 概要

HTTPでフロントエンドとの通信量を削減する方法の１つとして、コンテンツを特定の圧縮方式で圧縮して通信する方法がありますが、FastAPIでgzipによる圧縮が簡単にできるのでメモしておきます。

gzipでの通信のためには、

- リクエスト側がhttp headerの`Accept-encoding` 属性に、gzipを指定する
- サーバー側がgzipでの返却に対応する

の両方が必要です。

# How To

[公式ドキュメント](https://fastapi.tiangolo.com/advanced/middleware/#gzipmiddleware)　にあるように、ミドルウェアを挟むだけでOKです。

```python
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware

BIG_CONTENT =
"""
返却するコンテンツ
"""

app = FastAPI()

# この行を追加する
app.add_middleware(GZipMiddleware, minimum_size=1000)

@app.get("/")
async def main():
    return BIG_CONTENT
```

minimum_sizeで、圧縮を適用するコンテンツの最低サイズを指定することができます。

# 試してみる

上記のmiddlewareを追加した状態でAPIプロセスを起動させ、エンドポイントにcurlでリクエストしてみます。contentの内容は[Bible in English](https://www.o-bible.com/dlb.html)を利用しました。

まずはgzipをacceptしないでリクエストしてみます。

```bash
❯ curl -vv  localhost:8000  > out 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.77.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< date: Mon, 18 Apr 2022 08:41:39 GMT
< server: uvicorn
< content-length: 4457912
< content-type: application/json
<
{ [102270 bytes data]
100 4353k  100 4353k    0     0   158M      0 --:--:-- --:--:-- --:--:--  202M
* Connection #0 to host localhost left intact
```

4457912 bytes返って来ているのがわかります。

それでは gzipを許可した上でリクエストしてみます

```bash
❯ curl -vv --compressed localhost:8000 > out2  
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.77.0
> Accept: */*
> Accept-Encoding: deflate, gzip
>
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< date: Mon, 18 Apr 2022 08:31:23 GMT
< server: uvicorn
< content-length: 1393083
< content-type: application/json
< content-encoding: gzip
< vary: Accept-Encoding
<
{ [102223 bytes data]
100 1360k  100 1360k    0     0  3924k      0 --:--:-- --:--:-- --:--:-- 3966k
* Connection #0 to host localhost left intact
```

`Accept-Encoding: deflate, gzip` が指定されています。結果、レスポンスとして

- `content-encoding: gzip`
- `content-length: 1393083`

と変わっています。
この場合、4457912 bytes → 1393083bytes とおよそ７０％ほどデータサイズが削減されていることがわかります。

# 余談: gzipにするべきか

通信容量だけ見ればgzipにすべきですが、当然圧縮・解凍のためのCPUリソースとはトレードオフになります。そのため、コンテンツのサイズが一定以上のもののみ圧縮対象とすると良い(`minimum_size`の指定）と思います。

また、コンテンツ内容によっても圧縮率はだいぶ変わります。今回のテスト対象では70%ほど削減できましたが、画像等では圧縮の意味はほとんどないでしょう。

例えば「[Can gzip Compression Really Improve Web Performance?](https://www.pingdom.com/blog/can-gzip-compression-really-improve-web-performance/)」では、

- 事前に圧縮したファイルを配信しても、オンラインで圧縮してもそれほどパフォーマンスには大きな差はなかった
- HTMLやcombined JSなどは圧縮効果が高いものの、画像等では圧縮効果は小さい
- 容量が77%程度削減された結果、レスポンスタイムは15%ほど改善された

などの結果が出ていますので、特にサイズの大きなテキストコンテンツを扱うAPIで利用するとよさそうです。また APIではないですが、JSなど静的リソースもあらかじめ圧縮しておくとよさそうですね。