2025 年 10 月現在、世界中のウェブサイトの約 35 % が HTTP/3 での接続に対応しているようだ (https://w3techs.com/technologies/details/ce-http3). そろそろ仕事でも HTTP/3 接続への対応が必要になりそうなので、ひとまず HTTP/3 に対応した nginx サーバーを立てられないかを試してみる.

しかし、そもそも自分はウェブサイトを閲覧する際にいつも HTTP/3 で接続していたのだろうか? HTTP/2 のままだったのだろうか? このあたりがよくわかっていなかった.

なので、まずはブラウザから HTTP/3 接続が行われているのかどうかを確かめるところからはじめてみる. 幸い Cloudflare が提供している検証サイト (https://cloudflare-quic.com) にアクセスしてみると、現在のブラウザから HTTP/3 で通信できているのかを確認できるようだ.

試してみたところ、どうやら macOS Sequoia の M1 MacBook Pro からは Google Chrome 、 Firefox 、 Vivaldi などの全てのブラウザで HTTP/2 で接続されてしまっていた.

ちょっとややこしそうな気配を感じるので、先に curl などのコマンドラインプログラムから HTTP/3 での通信が行えるのかどうかを確かめてみることにした.

curl からの HTTP/3 接続を試す

curl は 2023 年にリリースされた v8.5.0 から HTTP/3 での接続に対応しているようなので、 macOS Sequoia 標準の curl ならば接続できるはずである (https://news.mynavi.jp/techplus/article/20231211-2838565/).

$ curl --version
curl 8.7.1 (x86_64-apple-darwin24.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.63.0
Release-Date: 2024-03-27
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM SPNEGO SSL threadsafe UnixSockets

しかし、 --http3 オプションを付けて接続を試してみると、エラーが発生した.

$ curl --http3 -v https://cloudflare-quic.com
curl: option --http3: the installed libcurl version doesn't support this
curl: try 'curl --help' or 'curl --manual' for more information

どうやら --http3 オプションを付与して HTTP/3 での通信を行うには v8.5.0 以降の curl があれば良いだけではなく、HTTP/3 に対応した libcurl がインストールされている必要があるようだ.

curl での HTTP/3 通信についての詳細 (https://curl.se/docs/http3.html) を見てみると、 curl から QUIC プロトコルを解釈するための 専用ライブラリにアクセスできるようになっている必要があるようだ. サイトを見る限り、 ngtcp2quicheOpenSSL 3.2+ QUIC の 3 通りの方法が用意されているようだが、 2025 年 10 月現在では ngtcp2 だけが安定版として見なされているようだ (Cloudflare 製の quicheOpenSSL 3.2+ QUIC を使うのはまだ experimental 扱い) .

したがって、今回は現時点で安定版になっている ngtcp2 を使った方法を採用してみる.

curlngtcp2 を有効にした状態でビルドするためには、 ngtcp2 以外に nghttp3 や QUIC をサポートする TLS ライブラリが必要となる. この TLS ライブラリとしては、同ページ中で OpenSSL、 quictls 、 GnuTLS 、 WolfSSL などが挙げられている.

例えば OpenSSL を TLS ライブラリとして使う場合には、以下の手順に従ってビルドする必要があるようだ.

# Build OpenSSL (version 3.5.0 or newer):
$ git clone --quiet --depth=1 -b openssl-$OPENSSL_VERSION https://github.com/openssl/openssl
$ cd openssl
$ ./config --prefix=<somewhere1> --libdir=lib
$ make
$ make install

# Build nghttp3:
$ cd ..
$ git clone -b $NGHTTP3_VERSION https://github.com/ngtcp2/nghttp3
$ cd nghttp3
$ git submodule update --init
$ autoreconf -fi
$ ./configure --prefix=<somewhere2> --enable-lib-only
$ make
$ make install

# Build ngtcp2:
$ cd ..
$ git clone -b $NGTCP2_VERSION https://github.com/ngtcp2/ngtcp2
$ cd ngtcp2
$ autoreconf -fi
$ ./configure PKG_CONFIG_PATH=<somewhere1>/lib/pkgconfig:<somewhere2>/lib/pkgconfig LDFLAGS="-Wl,-rpath,<somewhere1>/lib" --prefix=<somewhere3> --enable-lib-only --with-openssl
$ make
$ make install

# Build curl:
$ cd ..
$ git clone https://github.com/curl/curl
$ cd curl
$ autoreconf -fi
$ LDFLAGS="-Wl,-rpath,<somewhere1>/lib" ./configure PKG_CONFIG_PATH=<ngtcp2-root>/lib/pkgconfig --with-openssl=<somewhere1> --with-nghttp3=<somewhere2> --with-ngtcp2
$ make
$ make install

自分で手動でビルドする必要があるため、結構面倒である.

しかし実際のところ、 TLS ライブラリとして mac に最初からインストールされている LibreSSL を使うこともできるらしく、 macOS では LibreSSL を使って Homebrew 経由ですぐに HTTP/3 に対応した curl を配布してくれている人がいるようだ (https://zenn.dev/catatsuy/articles/28b4d8e865c6f7).

今回はこれをインストールしてみることにする.

$ brew tap catatsuy/tap
$ brew install curl-http3-libressl

configuremake が走ってコンパイルが行われるはずである.

インストール完了後、シェルを起動し直すとインストールした curl にパスが通った状態になっているはずである.

$ which curl
/opt/homebrew/bin/curl

$ curl --version
curl 8.16.0 (aarch64-apple-darwin24.2.0) libcurl/8.16.0 LibreSSL/4.1.0 zlib/1.2.12 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 nghttp2/1.65.0 ngtcp2/1.15.1 nghttp3/1.11.0 librtmp/2.3
Release-Date: 2025-09-10
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTP3 HTTPS-proxy IDN IPv6 Largefile libz NTLM SSL threadsafe UnixSockets zstd

改めて、再度 curl を使って https://cloudflare-quic.com にアクセスしてみる.

$ curl --http3 -v https://cloudflare-quic.com | head
* Established connection to cloudflare-quic.com (104.18.26.14 port 443) from 192.168.1.14 port 62879
* using HTTP/3
* [HTTP/3] [0] OPENED stream for https://cloudflare-quic.com/
* [HTTP/3] [0] [:method: GET]
* [HTTP/3] [0] [:scheme: https]
* [HTTP/3] [0] [:authority: cloudflare-quic.com]
* [HTTP/3] [0] [:path: /]
* [HTTP/3] [0] [user-agent: curl/8.16.0]
* [HTTP/3] [0] [accept: */*]
> GET / HTTP/3
> Host: cloudflare-quic.com
> User-Agent: curl/8.16.0
> Accept: */*
>
* Request completely sent off
< HTTP/3 200
< date: Wed, 08 Oct 2025 05:48:39 GMT
< content-type: text/html
< cf-ray: 98b359b9ff3dd5cf-NRT
< priority: u=3,i=?0
< server: cloudflare
< alt-svc: h3=":443"; ma=86400
< server-timing: cfExtPri

すると、確かに HTTP/3 で通信ができているようだ.


なお --http3 ではなく --http3-only オプションを使った curl --http3-only -v "https://…​" というコマンドの場合は絶対に HTTP/3 で接続しようとする. なので、接続失敗する場合にはこのコマンドの出力から何が問題なのか判定できるかもしれない.


--http3 オプションを付与すると HTTP/3 で接続できることは確認できたが、このオプションを付与せずに単に curl -v https://cloudflare-quic.com を実行した場合には HTTP/2 で接続しようとするようだ.

HTTP/3 は TCP ではなく UDP を使って通信しようとするため、環境によってはファイアウォールの設定で通信できない場合がある. したがって、 HTTP/3 での通信はいきなり HTTP/3 で通信するのではなく、最初だけは HTTP/1.1 や HTTP/2 でアクセスし、そのレスポンスにしたがって後続の接続を HTTP/3 に切り替えるかどうかを判断しているようだ.

なので、 curl のこの振る舞いは正常なものであるようだ.

よく見ると、レスポンス中に HTTP/3 で接続が可能なことを知らせる alt-svc: h3=":443"; ma=86400 という記述が含まれている.

$ curl -v https://cloudflare-quic.com | head
* Established connection to cloudflare-quic.com (104.18.26.14 port 443) from 192.168.1.14 port 51077
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://cloudflare-quic.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: cloudflare-quic.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.16.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: cloudflare-quic.com
> User-Agent: curl/8.16.0
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< date: Wed, 08 Oct 2025 05:49:36 GMT
< content-type: text/html
< cf-ray: 98b35b21cc0ad76b-NRT
< server: cloudflare
< alt-svc: h3=":443"; ma=86400
<
{ [1360 bytes data]

--alt-svc オプションを使うと、 Alt-Svc の結果をキャッシュしておけるらしい.

$ curl --alt-svc altsvc.cache https://cloudflare-quic.com

# altsvc.cache というファイルに Alt-Svc の結果が格納されている.
$ cat altsvc.cache
# Your alt-svc cache. https://curl.se/docs/alt-svc.html
# This file was generated by libcurl! Edit at your own risk.
h2 cloudflare-quic.com 443 h3 cloudflare-quic.com 443 "20251009 06:44:24" 0 0

--alt-svc altsvc.cache を付与したまま、もう一度同じアドレスにアクセスしてみると、今度は HTTP/2 ではなく HTTP/3 でのアクセスとなった.

$ curl --alt-svc altsvc.cache -v https://cloudflare-quic.com
* Alt-svc connecting from [h2]cloudflare-quic.com:443 to [h3]cloudflare-quic.com:443
* Host cloudflare-quic.com:443 was resolved.
* IPv6: (none)
* IPv4: 104.18.26.14, 104.18.27.14
*   Trying 104.18.26.14:443...
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* SSL connection using TLSv1.3 / TLS_CHACHA20_POLY1305_SHA256 / [blank] / UNDEF
* Server certificate:
*  subject: CN=cloudflare-quic.com
*  start date: Oct  1 12:45:04 2025 GMT
*  expire date: Dec 30 13:45:01 2025 GMT
*  subjectAltName: host "cloudflare-quic.com" matched cert's "cloudflare-quic.com"
*  issuer: C=US; O=Google Trust Services; CN=WE1
*  SSL certificate verify ok.
*   Certificate level 0: Public key type ? (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type ? (256/128 Bits/secBits), signed using ecdsa-with-SHA384
*   Certificate level 2: Public key type ? (384/192 Bits/secBits), signed using ecdsa-with-SHA384
* Established connection to cloudflare-quic.com (104.18.26.14 port 443) from 192.168.1.14 port 58039
* using HTTP/3
* [HTTP/3] [0] OPENED stream for https://cloudflare-quic.com/
* [HTTP/3] [0] [:method: GET]
* [HTTP/3] [0] [:scheme: https]
* [HTTP/3] [0] [:authority: cloudflare-quic.com]
* [HTTP/3] [0] [:path: /]
* [HTTP/3] [0] [user-agent: curl/8.16.0]
* [HTTP/3] [0] [accept: */*]
* [HTTP/3] [0] [alt-used: cloudflare-quic.com:443]
> GET / HTTP/3
> Host: cloudflare-quic.com
> User-Agent: curl/8.16.0
> Accept: */*
> Alt-Used: cloudflare-quic.com:443
>
* Request completely sent off
< HTTP/3 200
< date: Wed, 08 Oct 2025 06:45:37 GMT
< content-type: text/html
< cf-ray: 98b3ad2bde7dd5ca-NRT
< priority: u=3,i=?0
< server: cloudflare
* Added alt-svc: cloudflare-quic.com:443 over h3
< alt-svc: h3=":443"; ma=86400
< server-timing: cfExtPri
<

したがって、初回だけ HTTP/2 でアクセスし、 2 回目以降は HTTP/3 でアクセスするという振る舞いを確認できた.

ウェブブラウザからの HTTP/3 接続しようと思ったらセキュリティソフトに妨害されていた

curl では HTTP/3 での接続が行えることを確認できた. しかし、 Chrome や Firefox などのウェブブラウザからの https://cloudflare-quic.com へのアクセスは相変わらず HTTP/3 ではなく HTTP/2 で接続されてしまうままである. この挙動についてはこの後相当試行錯誤を繰り返し、苦しまされることとなった…​

(なお、 Chrome の拡張機能として HTTP Indicator (https://chromewebstore.google.com/detail/http-indicator/hgcomhbcacfkpffiphlmnlhpppcjgmbl?hl=JA) という拡張を入れておくと、 HTTP/3 で接続しているかどうかを一目で確認できて便利である. HTTP/3 接続の場合はオレンジ色のカミナリマークが表示され、 HTTP/2 接続の場合は青色のカミナリマークが表示される.)

実は、ウェブブラウザからの HTTP/3 接続を妨害していたのは セキュリティソフト (Avast) のフィルタリング設定 であった. システム設定 を開き、 ネットワーク タブの中の フィルタ を開いて Avast Security- (マイナス) ボタンで除去してからアクティビティモニタから Avast 関連のプロセスを再起動させてから再び https://cloudflare-quic.com にアクセスしてみると、 HTTP Indicator のカミナリマークがオレンジ色になり、接続が HTTP/3 で行われるようになったのを確認できた.

なお、一旦このフィルタリングの影響で HTTP/3 接続がブロックされていると、ブラウザのキャッシュをクリアしないと中々 HTTP/3 で接続しようとせず、フィルタリングを無効化しているにも関わらず HTTP/2 で接続してしまうようだった (トラップすぎる…​).

これでひとまず、ブラウザとコマンドラインから HTTP/3 接続ができることを確認できた.

HTTP/3 に対応した nginx を Docker で動かす

次に、 HTTP/3 が有効な nginx を Docker を用いて動かしてみる (https://zenn.dev/matsubokkuri/articles/docker-http3).

ただし、 HTTP/3 は HTTP/2 と同様に https での接続が必須であるので SSL 証明書が必要である. 証明書は通常 Let’s Encrypt を使って発行することが多いと思われるが、ローカルで開発するだけなのに DNS の登録が必須だったり面倒である. したがって、今回は mkcert を使った自己署名証明書 (https://qiita.com/k_kind/items/b87777efa3d29dcc4467) を使って試そう…​と思ったが、 これが思わぬ落とし穴であった. というのも、 mkcert で作った証明書を読み込ませて nginx を立ち上げると、 curl では問題なく HTTP/3 で接続できるのにウェブブラウザからは何度試しても絶対に HTTP/3 ではなく HTTP/2 で接続されてしまう という謎挙動になってしまったからである. この原因はいまいちよくわかっていないが、 これから試される方は面倒でも Let’s Encrypt を使って本物の SSL 証明書を使って試されることをオススメします…​

したがって、まずは Let’s Encrypt で SSL 証明書を発行することとする. 今回はローカル開発環境上で nginx が HTTP/3 で通信できるかを確かめたいので、 127.0.0.1 に解決される独自ドメインを取得し、そのドメインのための証明書を用意する (https://scrapbox.io/nwtgck/127.0.0.1%E3%81%ABLet'_Encrypt%E3%81%A7%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%82%92%E7%99%BA%E8%A1%8C%E3%81%97%E3%81%A6https%E3%81%A7%E3%81%AE%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%8B%E7%99%BA%E3%81%A8%E6%9C%AC%E7%95%AA%E3%81%AE%E5%B7%AE%E7%95%B0%E3%82%92%E4%BD%8E%E6%B8%9B%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AA%E6%89%8B%E9%A0%86). 127.0.0.1 に解決されるため、誰が、このドメインにアクセスしたとしても単に自分自身のマシンを参照することになるので安全である. 事前に独自ドメイン (今回の場合は localhost.annpin.com とする) を取得し、これが 127.0.0.1 に解決されるよう DNS に A コードとして登録しておくこと.

Docker を使って certbot のコンテナを立ち上げ、 Let’s Encrypt の証明書を取得してみる. 認証については DNS の TXT レコードを使って行うので、途中で指定されたとおりに自分で DNS に TXT レコードを追加すること.

$ mkdir letsencrypt
$ docker run -it \
  --rm \
  -v ./letsencrypt:/etc/letsencrypt \
  -e TZ="Asia/Tokyo" \
  certbot/certbot:latest \
  certonly \
  --manual \
  --preferred-challenges dns \
  -m メールアドレス \
  -d localhost.annpin.com

すると、以下のような出力が得られる. ここでは途中、 DNS に TXT レコード (この場合は _acme-challenge.localhost.annpin.com) を作る必要があるので注意. 完了したら Enter キーを押すと良い.

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at:
https://letsencrypt.org/documents/LE-SA-v1.5-February-24-2025.pdf
You must agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y
Account registered.
Requesting a certificate for localhost.annpin.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.localhost.annpin.com.

with the following value:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.localhost.annpin.com.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/localhost.annpin.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/localhost.annpin.com/privkey.pem
This certificate expires on 2026-01-06.
These files will be updated when the certificate renews.

NEXT STEPS:
- This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided. To renew this certifi
cate, repeat this same certbot command before the certificate's expiry date.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

これにより、 ./letsencrypt フォルダ内に証明書が配置された.

続けて、nginx コンテナを起動するために docker compose の設定ファイルと nginx の設定ファイルを作成する.

nginx の公式ページ (https://nginx.org/en/docs/quic.html) を見てみると、 nginx 1.25.0 から QUIC および HTTP/3 に対応したと書いてある. なので、とりあえず最新の公式イメージで HTTP/3 が使えるようになっているのかを確かめたい. https://it7c.hatenadiary.org/entry/20160318/1458288392 を参考に、 nginx に読み込まれているモジュール一覧を表示させてみる.

まず、 nginx の最新イメージのコンテナを起動する.

$ docker run -it nginx:1.29.2 /bin/sh

読み込まれているモジュール一覧を確認する.

$ 2>&1 nginx -V | tr -- - '\n' | grep _module
http_addition_module
http_auth_request_module
http_dav_module
http_flv_module
http_gunzip_module
http_gzip_static_module
http_mp4_module
http_random_index_module
http_realip_module
http_secure_link_module
http_slice_module
http_ssl_module
http_stub_status_module
http_sub_module
http_v2_module
http_v3_module
mail_ssl_module
stream_realip_module
stream_ssl_module
stream_ssl_preread_module

すると、 http_v3_module が有効になっていることが確認できる. よって単に最新の nginx イメージを持ってくれば HTTP/3 での接続を試すことができそうだ.

docker compose ファイルを作成する.

compose.yaml
services:
  nginx:
    image: nginx:1.29.2
    ports:
      - '443:443/udp'
      - '443:443/tcp'
    restart: always
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      # Let's Encrypt 証明書をマウント
      - ./letsencrypt/archive/localhost.annpin.com/fullchain1.pem:/etc/ssl/certs/server.crt    # サーバー証明書
      - ./letsencrypt/archive/localhost.annpin.com/privkey1.pem:/etc/ssl/certs/server.key      # 秘密鍵

nginx の設定ファイルも作成する.

nginx.conf
worker_processes  1;

events {
  worker_connections  1024;
}

http {
  server {
    listen               443 ssl;            # Enable SSL
    listen               443 quic reuseport; # Enable QUIC and HTTP/3.
    # http2 on; をコメント化して無効化すれば HTTP/1.1 で接続を試し、 HTTP/3 に対応していれば HTTP/3 で通信するという振る舞いになる.
    http2                on;
    http3                on;
    add_header           Alt-Svc 'h3=":443"; ma=86400';    # 86400sec = 24hour

    server_name          localhost.annpin.com;
    ssl_protocols        TLSv1.2 TLSv1.3;    # TLSv1.3 is required for QUIC

    ssl_certificate      /etc/ssl/certs/server.crt;
    ssl_certificate_key  /etc/ssl/certs/server.key;

    # Just for testing
    access_log           /dev/stdout;
    error_log            /dev/stderr debug;

    location / {
      # Default page
      # 以下は指定すると HTTP/3 接続されなくなるようなので注意.
      # add_header X-Protocol $server_protocol always;
      root   /usr/share/nginx/html;
      index  index.html index.htm;
    }
  }
}

この状態で docker compose up -d でコンテナを立ち上げ直し、ウェブブラウザ (念の為シークレットモード) で https://localhost.annpin.com にアクセスすると、 HTTP/3 で接続できることが確認できた.

自サイトを HTTP/3 化しようと思った場合、2025 年現在では "クライアントのセキュリティソフトの状態" とか、 "ファイアウォールで 443:UDP が止められていないか" とか、考慮すべきトラップが多いなと思う…​

なお、ファイアウォールの設定などの何らかの理由で HTTP/3 接続ができない場合には HTTP/2 接続にフォールバックされるようになっているが、 nginx.confhttp2: off; と書いておけば、 HTTP/2 ではなく HTTP/1.1 での接続にフォールバックさせることもできる. ファイルアップロードなどの特定のユースケースでは HTTP/2 ではなく HTTP/1.1 のほうが性能が出るケースもあるようなので、 http2: on; を有効にするかどうかはその都度判断する必要がある (https://www.reddit.com/r/node/comments/1cemrgp/benchmarking_http2_vs_http11_results_not_as/?show=original).

HTTP/1.1 よりも HTTP/2 のほうが性能が劣化してしまう問題は TCP レベルの HoL (Head of Line) ブロッキング問題が原因であるようだ. 詳しくは https://dorapon2000.hatenablog.com/entry/2021/04/11/180740 などで説明されている.

大筋としては、大体以下のような事情であるようだ.

  1. 複数のリクエストをサーバーに送信する場合に HTTP/1.1 では TCP コネクションを介した送信が順次実行されるという性質上、先行する HTTP リクエストの処理が終わらないと後続の HTTP リクエストがずっと順番待ちになってしまっていつまでも処理されないという問題 (HTTP レベルの HoL ブロッキング問題) があった.

  2. HTTP/2 では TCP コネクションの多重化によって複数の HTTP リクエスト/レスポンスを並行に送信できるようにしたことでこの問題を解決したが、その一方で TCP パケットが再送になると再送が完了するまで後続のパケットが全てブロックされてしまうという TCP レベルの HoL ブロッキング問題が生じてしまっていた.

  3. HTTP/1.1 の HTTP レベルの HoL ブロッキング問題は深刻であったため、主要なウェブブラウザは HTTP/1.1 では最大 6 つの独立した TCP コネクションを開けるようにすることで「6 つのうち 1 つの通信の際に再送が生じても他の 5 つの通信には影響が生じない」 (代わりに扱う TCP コネクションの数が増えるので、サーバー側の負荷が高まる) という挙動となっていたが、HTTP/2 では 1 つの TCP コネクションを全てのリクエストで共有して利用するので、 1 つのパケットが再送になると後続のパケットが全てブロッキングされてしまうようになった.

まぁ、 HTTP/3 ではこの HTTP/2 の問題を克服されているとのことなので HTTP/3 が広く普及してくれることが一番ではあるのだが、 UDP がファイアウォールでブロックされたり、 上に書いたようにセキュリティソフトのフィルタリングで勝手に妨害されてしまうこともあるようなので、普及にはもう少し時間がかかりそうだ…​ (HTTP/2 と HTTP/3 の性能を比較した論文が arxiv で公開 (https://arxiv.org/pdf/2409.16267) されているが、チラッと見た感じ、パケットロスが多い (4 % 程度) 通信環境では HTTP/3 通信が HTTP/2 の 10 倍近い性能を出すケースがあるようだ.)

References