🦔

Remix on Cloudflare Pagesして気付いたこと

2024/10/24に公開

はじめに

最近Cloudflare Pagesを使ってキュレーションサイトを作成しました。
最初はRemixで作成していたのですが、ある問題が完成間近で発覚して、Nextjsで作り直しました。
この記事では、その問題について振り返りをしてみます。
https://zenn.dev/aozou/articles/4d04410376651f

問題の発覚

実装も終盤に差し掛かり、SEO対策として各ページに構造化マークアップを実装しました。
例えばWebマンガ更新一覧のページでは、その日更新されたWebマンガの情報(画像/タイトル/作者)を検索エンジンに伝えるために、Remixのmetaを使った下記のようなコードになります。


Webマンガ更新一覧

構造化マークアップ処理を抜粋
export const meta: MetaFunction<typeof loader> = ({ data }) => {
    const description =
        '少年ジャンプ+、ヤンマガWeb、となりのヤングジャンプ、裏サンデー、マガポケ、くらげバンチなど多くのWebマンガの更新をまとめてチェックできます。';

    if (!data) {
        return [
            { title: 'まんがHub - Webマンガ更新情報' },
            {
                name: 'description',
                content: description,
            },
            {
                property: 'og:image',
                content: 'https://manga-hub.work/images/ogp/web-manga.png',
            },
            {
                property: 'og:title',
                content: 'まんがHub - Webマンガ更新情報',
            },
        ];
    }

    const { updatedWebSeries, dayOfWeek, date } = data as ResponseData;
    const title = `まんがHub - Webマンガ更新情報 ${formatDate(date, 'MM/DD')} (${dayOfWeek})`;
    const schemaMarkup = {
        '@context': 'https://schema.org',
        '@type': 'Article',
        headline: title,
        description,
        url: 'https://manga-hub.work/web-manga',
        datePublished: new Date().toISOString(),
        dateModified: new Date().toISOString(),
        author: {
            '@type': 'Organization',
            name: 'まんがHub',
        },
        publisher: {
            '@type': 'Organization',
            name: 'まんがHub',
            logo: {
                '@type': 'ImageObject',
                url: 'https://manga-hub.work/logo.png', 
            },
        },
        mainEntityOfPage: {
            '@type': 'WebPage',
            '@id': 'https://manga-hub.work/web-manga',
        },
        articleBody: description,
        about: {
            '@type': 'ItemList',
            itemListElement: updatedWebSeries.map((webSeries, index) => ({
                '@type': 'ListItem',
                position: index + 1,
                item: {
                    '@type': 'ComicStory',
                    name: webSeries.name,
                    author: {
                        '@type': 'Person',
                        name: webSeries.author,
                    },
                    publisher: {
                        '@type': 'Organization',
                        name: webSeries.site.name,
                        url: webSeries.site.url,
                    },
                    url: webSeries.url,
                    image: webSeries.imageUrl,
                },
            })),
        },
    };
    return [
        { title },
        {
            name: 'description',
            content: description,
        },
        { 'script:ld+json': schemaMarkup },
        {
            property: 'og:image',
            content: 'https://manga-hub.work/images/ogp/web-manga.png',
        },
        {
            property: 'og:title',
            content: title,
        },
    ];
};

この実装を本番に反映し、いざページにアクセスすると下記のようなエラーが多発する状態になりました。


実際に表示された画面

原因について

メトリクスを確認してみると、CPUの処理時間がLimitsの10msを超えていることが原因のようです。
ドキュメントに詳細が無かったので使った肌感の推測になりますが、厳密に10msではなく少しオーバーしても見逃してくれます。
また1リクエストに対しての制限ではなく、一定期間内の平均値を参照していそうな気がします。


Cloudflare管理画面

以前から兆候があった

実は構造化マークアップの処理を入れる以前も、ページのリロードを何度も行うと、時々同様のエラーが発生することがありました。
これらを考慮すると原因について下記の推測できます。

  • 元々の処理が限界に近づいていた
  • 構造化マークアップの処理もそこそこ重く、トドメになってしまった

JSONのシリアライズが悪そう

ただ、先程の構造化マークアップのコード自体はオブジェクトを作成してreturnしているだけなので、特に重い処理では無さそうです。
そこで、実際にブラウザに返されるHTMLを見てみると先程のオブジェクトがJSONとして埋め込まれていることが分かります。
この事からファーストビューのレンダリングでJSONへのシリアライズ処理が行われており、おそらくこれが原因であると判断しました。


そこそこのサイズのJSONが埋め込まれている(50KBぐらいある)

元々の処理でも、シリアライズ/デシリアライズを行っていた

上記の推測を行き着くと同時に、既存の処理でも同じ問題があることに気づきました。
マンガの一覧自体は、1日に数回だけ更新されることがあるため、キャッシュできるデータになります。そのためKVを使って、D1から取り出したデータをキャッシュしていました。
KVに保存する際はオブジェクトをJSONに、KVから取り出す際はJSONをオブジェクトへと変換を行っていたので、これらの処理もそこそこネックなっていそうです。

SSRでやっている処理を整理

雑に図にしてみると、下記の処理が大半を占めている気がしてきました。

  1. JSONのシリアライズ/デシリアライズ
  2. ReactコンポーネントをHTMLへの変換(コンポーネントのボリュームや内容に比例しそう)

対策

構造化マークアップのJSONを削ったり、既存の処理の最適化で凌ぐことも考えたのですが、SSGに乗り換えることにしました。
決断した理由は、下記のとおりです。

  1. SSRで続ける限り今後なにかの処理を追加する度に、同様のエラーに怯え続ける可能性が高いこと。
  2. Reactコンポーネントの肥大化にも影響される可能性があること。
  3. SSGであれば上記のリスクが0になること。
  4. サイトの特性を考えると、SSGのビルドフックがユーザーイベントに依存せず、決まったタイミングで行えば十分であることから、SSGに適してそう。

移行のコスト

移行自体は特に難航しませんでした。
大きいな理由としてはReactコンポーネントをそのまま使えたため、バックエンドの処理(loader/action)だけHonoに載せ替えるだけで済みました。
そのため、1画面移行してしまえば、後はそのノウハウを使って単純作業の繰り返しになります。

最後に

本来ならば、先述した原因の仮説が正しいかデモを作って検証するべきなのですが、当時は面倒臭がってサボってしまいました。
検証自体は面白そうなので、次回チャレンジしてみようと思います!

【追記】
実際に検証しました!良ければ参考にしてください
https://zenn.dev/aozou/articles/2836666b835cb9

気付いたこと

CPU処理時間が10msという厳しい条件下で、SSRで色々詰め込むのって結構無理ゲーなのでは?
やりたいことがシンプルな場合は問題ないと思いますが、将来的にやりたいことが増えていく可能性がある場合、どこかのタイミングで10msという壁にぶつかりやすい構成な気がします。

Discussion