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

2024/02/20に公開

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

現在、弊社で運営しているオウンドメディア「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