KiSAKU
技術系約6分で読めます

ソース解説(4) 速くしたら、デプロイが止まった

公開

記事のデータは、共用サーバー上のWordPressから取ってきています。この部分は2回作り直しました。

最初は「常に最新を取る」形。速度の問題が出て、キャッシュする形へ。すると今度は、デプロイが止まるようになりました。

連載「ソース解説」(全10回)

第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専用でした。書いた側からはエラーも出ないので、速くなっていないという結果からしか気づけません。

次に、速くする変更は、壊れ方も一緒に変えること。事前に組み立てる形にした時点で、失敗する場所が「表示」から「公開作業」へ移っていました。速度の話をしているつもりでした。

そして、相手はたまに失敗する、を前提に書くこと。共用サーバーは混みます。投げ直す、取れなければ薄いものを返す、どちらも「相手が常に応えてくれる」という前提を外した結果です。前提を外すと、コードは少し増えますが、止まらなくなりました。

この記事を共有する

Xでポストはてブ
← ブログ一覧へ戻る