ソース解説(4) 速くしたら、デプロイが止まった
記事のデータは、共用サーバー上のWordPressから取ってきています。この部分は2回作り直しました。
最初は「常に最新を取る」形。速度の問題が出て、キャッシュする形へ。すると今度は、デプロイが止まるようになりました。
第4回。前回:旗を折るのではなく、読み込まない 次回:文字列を自分で組み立てる場所(近日公開)
問い合わせ部分のコードと、キャッシュを置いた場所、そして相手が「たまに失敗する」前提で書き直した話です。速くするための選択が、別の場所の壊れやすさを生んでいました。
最初の形と、その代償
以前は、問い合わせのたびに「保存しないで取ってきて」と指定していました。記事を公開したらすぐ反映される、という点では狙いどおりです。
ただしこの指定があると、ページをあらかじめ組み立てておくことができません。訪問者が来るたびに共用サーバーへ問い合わせが飛び、その待ち時間がそのまま表示速度になります。
実測で、最初の1バイトが返るまで約1.7秒でした。
キャッシュを、どこに置くか
ここで1つ引っかかりました。
Next.jsには、通信の結果を保存しておく仕組みがあります。ところがそれが効くのはGETで取りに行ったときだけです。GraphQLの問い合わせはPOSTで送るので、通信のところに保存の指定を書いても素通りします。
指定を書いた側から見ると、設定はしてあります。エラーも警告も出ません。速くなっていないことでしか気づけません。
「効くはずの場所に書いたのに効かない」ときは、その仕組みが何を対象にしているかを読み直す必要がありました。
そこで、通信ではなく「関数の戻り値」を保存する仕組みに切り替えました。
export const getPosts = unstable_cache(
async () => {
const data = await graphqlClient.request(GET_POSTS);
return data.posts?.nodes ?? [];
},
["posts"],
{ revalidate: 300, tags: ["wordpress"] },
);
これは通信の種類に関係なく効きます。引数も自動で鍵に含まれるので、getPostBySlug("a") と getPostBySlug("b") は別物として保存されます。
末尾の ?? [] は「取れなかったら空の配列」。件数が0件なのと、取得に失敗したのを同じ形で返しています。一覧を表示する側から見れば、どちらも「並べるものが無い」なので、扱いを分ける意味がありません。
tags: ["wordpress"] は、あとから「この印の付いた保存を今すぐ捨てる」ためのものです。
WordPressで記事を公開したときにVercelへ通知を送る仕組みを足せば、5分待たずに反映できます。いまは使っていませんが、後から足せるように印だけ置いています。後で足すときにデータの形を変えずに済みます。
速くしたら、デプロイが止まった
ここからが本題です。
ページをあらかじめ組み立てるということは、組み立てるタイミングでCMSへ問い合わせに行くということです。訪問者が来たときではなく、公開作業の途中で。
共用サーバーのWordPressは、混み具合によっては処理しきれずエラーを返すことがあります。以前は動的に組み立てていたので、そのときは「その1回の表示が失敗する」だけで済んでいました。
ところが事前に組み立てる形にしたことで、1回の失敗がデプロイ全体を止めるようになりました。実際に止まりました。
遅いが止まらない形から、速いが止まる形へ移っていました。速度の話をしているつもりで、失敗したときに何が起きるかを一緒に変えていたことになります。
失敗する前提で書き直す
対処として、投げ直す処理を入れました。
const MAX_ATTEMPTS = 3;
const RETRY_DELAY_MS = [1000, 3000];
待ち時間を置いているのは、すぐ投げ直しても、混んでいる相手には効かないからです。同じ瞬間にもう一度叩くのは、混雑を増やしているだけになります。
そして、投げ直す相手を選んでいます。
if (response.status >= 500 && attempt < MAX_ATTEMPTS - 1) {
continue;
}
return response;
| 返ってきたもの | 意味 | 扱い |
|---|---|---|
| 500番台 | 相手側の不調 | 投げ直す |
| 400番台 | こちらの要求が違う | 投げ直さない |
| 通信そのものの失敗 | 届いていない | 投げ直す |
400番台を投げ直さないのは、何度試しても同じ結果になるからです。要求の中身が間違っているのに3回投げるのは、単に3倍待つだけです。
取れなかったときに、何を返すか
RSSとサイトマップでは、さらに一段踏み込んでいます。
let posts = [];
try {
posts = await getPostsForFeed();
} catch (error) {
console.warn("[feed] 記事一覧を取得できませんでした", error);
}
失敗しても、そのまま先へ進みます。記事が0件のフィードになりますが、フィードとしては成立する形で返ります。
ここで例外をそのまま外へ出すと、そのページ自体がエラーになります。購読アプリによっては「壊れている」と判断して購読を外します。
一時的な不調のせいで、購読を失うのは割に合いません。中身が薄いものを返すほうが、被害が小さく済みます。
サイトマップも同じで、記事が取れなければ固定ページの分だけを返します。不完全なサイトマップは、存在しないサイトマップよりは役に立ちます。
問い合わせの道具を、あえて小さいものにした
GraphQLの問い合わせには、高機能なものも使えます。それは選びませんでした。
高機能なものが持っているのは、主にブラウザ側でのキャッシュ管理です。このサイトはサーバー側で組み立ててHTMLを返す形が中心なので、その恩恵をあまり受けません。受けない機能のぶんだけ、覚えることと依存が増えます。
いま使っているのは「問い合わせ文を投げて結果を受け取るだけ」のものです。必要になったら後から差し替えられるよう、問い合わせの実行部分は1か所に集めてあります。
今回の学び
まず、効くはずの設定が効かないときは、対象を読み直すということ。保存の指定はGET専用でした。書いた側からはエラーも出ないので、速くなっていないという結果からしか気づけません。
次に、速くする変更は、壊れ方も一緒に変えること。事前に組み立てる形にした時点で、失敗する場所が「表示」から「公開作業」へ移っていました。速度の話をしているつもりでした。
そして、相手はたまに失敗する、を前提に書くこと。共用サーバーは混みます。投げ直す、取れなければ薄いものを返す、どちらも「相手が常に応えてくれる」という前提を外した結果です。前提を外すと、コードは少し増えますが、止まらなくなりました。
この記事を共有する
関連記事

ソース解説(2) 目次は本文から作る
本文が文字列として届くこと、そこから見出しを拾う方法、既にあるidを勝手に変えてはいけない理由、そして読んでいる位置をどう追いかけているかです。

ソース解説(1) 全体の地図と、境目の引き方
このサイトの作りについては何度か書きましたが、中のコードそのものには触れていませんでした。 ここから何本かに分けて、実際のファイルを開きながら説明していきます。まずは全体の地図と、読むために知っておいたほうがいい前提から […]

推測させるのをやめる
sitemap.xml・robots.txt・構造化データ。バラバラの施策に見えて、やっていることは同じだった。これまで相手に推測させていたことを、こちらから明示的に伝える。そして実装中に見つけた、一覧用のクエリを流用すると21本目から静かに漏れ始めるという落とし穴。