Next.jsのApp RouterをCloudflare Pagesにアップする際にcheerioをCSRにする

2024/02/20に公開3

タイトルに要点を入れようとしたら長いタイトルになりましたがタイトルままの内容です。

現在、弊社で運営しているオウンドメディア「to-R Media」のバージョンアップを行っています。

to-R MediaはもともとはNext.jsのPage Routerで開発しておりSSGを行って静的ファイルとしてCloudflare Pagesにデプロイしておりましたが、今回のバージョンアップではNext.jsのApp Routerに変更を行いSSRでCloudflare Pagesにデプロイをする予定です。

ところが、Cloudflare Pagesにアップしたところ記事詳細ページで500エラーが頻出して動作が安定しませんでした。

Cloudflare PagesのSSRで利用されているCloudflare Workersは無料プランだとCPUの実行時間が10msと結構タイトで重い処理が含まれていると処理しきれないようです。

https://developers.cloudflare.com/workers/platform/limits/#worker-limits

コードを調べてみるとcheerioを利用したコードハイライトの処理が重く10msの制限を超過していたようです。

もともとのSSRのコード

Nxet.js はデフォルトではReact Server Componentsが採用されるため以下のようにSSRでレンダリングされるように実装していました。

components/CodeBlock.tsx
import * as cheerio from 'cheerio';
import hljs from 'highlight.js';

export default async function CodeBlock({ article }: { article: string }) {
  const $ = cheerio.load(article, null, false);

  $('pre code').each((_, elm) => {
    const result = hljs.highlightAuto($(elm).text());
    $(elm).html(result.value);
    $(elm).removeAttr('lang');
    $(elm).addClass('hljs');
  });

  const highlightedArticle = $.html();

  return (
    <div
      className="p-article-body"
      dangerouslySetInnerHTML={{ __html: highlightedArticle }}
    />
  );
}

修正したCSRのコード

CodeBlock.tsxではハイライト処理は行わずにHTMLをそのまま描画するにとどめます。

components/CodeBlock.tsx
import CodeBlockHighlight from '@/components/CodeBlockHighlight';

export default async function CodeBlock({ article }: { article: string }) {
  return (
    <CodeBlockHighLight>
      <div
        className="p-article-body"
        dangerouslySetInnerHTML={{ __html: article }}
      />
    </CodeBlockHighLight>
  );
}

CodeBlockHighlight.tsxuse clientを利用してReact Client Componentsとして実装してCSR時にcheerioによるコードハイライト処理が実行されるようにします。

components/CodeBlockHighlight.tsx
'use client';

import * as cheerio from 'cheerio';
import hljs from 'highlight.js';
import { useEffect } from 'react';

type Props = {
  children: React.ReactNode;
};

const CodeBlockHighlight = ({ children }: Props) => {
  useEffect(() => {
    const articleBody = document.querySelector('.p-article-body');
    if (!articleBody) {
      return;
    }
    const $ = cheerio.load(articleBody.innerHTML, null, false);

    $('pre code').each((_, elm) => {
      const result = hljs.highlightAuto($(elm).text());
      $(elm).html(result.value);
      $(elm).removeAttr('lang');
      $(elm).addClass('hljs');
    });

    articleBody.innerHTML = $.html();
  });

  return <>{children}</>;
};

export default CodeBlockHighlight;

こうすることで以下のようにHTMLが描画されてからタイムラグがあってコードハイライトが適用されます。

こちらに変更したところCloudflare Pagesでも安定して表示されるようになりました。

Cloudflare PagesでApp Routerを利用する場合はCSRで大丈夫な処理は極力CSRに寄せてあげると安定するので注意してください。

株式会社トゥーアール

Discussion

taku3taku3

初めまして、半年前の記事に恐れ入ります。
こちらで遭遇されていたエラーですが、
Application error: a server-side exception has occurred (see the server logs for more information).
のようなメッセージだったか覚えていらっしゃいますでしょうか?
fetch周りをいじっており、ローカルのbuild/ssrでは問題ないのですがcloudflare pagesだと上記エラーになり、サーバでfetch中にタイムアウトしている可能性を考えています。
お返事いただけましたら幸いです。

西畑一馬西畑一馬

@taku3 すいません、ちょっとエラーメッセージは覚えてないですね。すいません。

taku3taku3

ご返事いただきありがとうございます。畏まりました。