Home>サイトのパフォーマンス改善を行いました

サイトのパフォーマンス改善を行いました

最終更新:2023-11-23

以前、こちらで説明したサイトの実装内容から、改善を行った点があるので、紹介していきたいと思います。

改善内容としては、これまで、MarkdownからHTMLに変換する処理のうち、一部をクライアント側で処理するようにしていましたが、全てビルド時にするように改善しました。

これによって、アクセス時にHTMLをASTに変換するparserをダウンロードする必要がなくなったため、通信料も減りました。

具体的には、

Before
一度ビルド&デプロイ時にHTML変換したものを、ユーザーがアクセスするときに再びrehype-parseで分解して、コードのハイライトをしたり、画像をnext/imageに置き換えたり
を、
After
ビルド時にすべて行えるようにrehypeプラグインを作る
に改善しました。

作ったrehypeプラグインを順に紹介していきたいと思います。

画像のサイズをimgタグに付与するプラグイン

該当のコードはここで、以下の様になっています。

export default function rehypeImageSize(): Transformer {
  return (tree) => {
    visit(tree, "element", (node: Element) => {
      if (["img"].includes(node.tagName)) {
        const { width, height } = sizeOf("public" + node.properties!.src);
        node.properties!.width = width;
        node.properties!.height = height;
      }
    });
  };
}

挙動としては、画像のサイズ(縦横の解像度)を取得し、imgタグのプロパティとして登録しています。これによって、意図したサイズで記事内に画像を表示できます。

コードのハイライトを行うためのプラグイン

該当のコードはここで、以下のようになっています。

export default function rehypeMyCodeBlock(): Transformer {
  return (tree) => {
    visit(tree, "element", (node: Element) => {
      if (["pre"].includes(node.tagName)) {
        // block
        const preEl = node;
        if (preEl.properties) {
          preEl.properties.className ??= [];
          Array.isArray(preEl.properties.className) &&
            preEl.properties.className.push("codeblock group");
        }
        const codeEl = node.children[0] as Element;
        if (codeEl.properties) {
          codeEl.properties.className ??= ["hljs"];
          Array.isArray(codeEl.properties.className) &&
            codeEl.properties.className.push("codeblock__txt");
        }
        preEl.children.unshift(copyBtnHst as Element);
      }
      if (["code"].includes(node.tagName)) {
        if (!node.properties!.className) {
          // inline
          node.properties!.className = ["codeblock__inline"];
        }
      }
    });
  };
}

挙動としては、sampleのようなインラインのコードでない上のようなコードブロックにclasshljsを追加し、コードのbutton要素を追加し、コードのコピー機能を実現するために必要なclassの付与を行っています。

コピー機能自体は、ここの実際に記事のレンダリングを行う箇所にuseEffectとして実装しています。

コード内容は以下のとおりです。

useEffect(() => {
    const codeBlockEls = document.querySelectorAll(".codeblock");
    codeBlockEls.forEach((el) => {
      const copyBtn = el.querySelector<HTMLElement>(".codeblock__btn");
      const txt = el.querySelector<HTMLElement>(".codeblock__txt")?.textContent || "";
      copyBtn?.addEventListener("click", () => {
        navigator.clipboard.writeText(txt);
        copyBtn.innerHTML = "Copied!";
        setTimeout(() => (copyBtn.innerHTML = "Copy"), 2000);
      });
    });
}, []);

先程紹介したプラグインで付与したclassを目印に、コード内容を取り出し、クリップボードにコピーしています。

これまでは、ここまでの処理をreact-syntax-highlighterやreact-copy-to-clipboardを用いて実装していました。

しかし、このように、良いコードであるかどうかは別として、出来上がったHTMLをあとから分解してコピー機能を実装したコンポーネントに置き直さなくても、コピー機能を実装できることが分かります。

その他

その他に作ったプラグインとしては、以下の3つがあります。

detailsタグのスタイル修正

detailsタグとは、以下のようなMarkdown表現です。

見出し
  • 本文1
  • 本文2
  • 本文3

このdetailsタグのスタイルを変更する際に、ボタンのクリック可能範囲が狭まってしまう問題がありました。

これを解決するにはcssだけでは無理で、中の要素をdivタグ囲むような処理が必要だったため、rehypeプラグインを作って対応しました。

コードはこちらです。

外部リンクであれば新しいタブで開くようにする

内部リンクでは同じタブで、外部リンクであれば新しいタブで開くようにして、UXの改善に取り組みました。

コードはこちらです。

テーブルを横スクロール可能に

特にスマホの画面だと、この記事などではテーブルが横スクロールできず、見にくいという問題があったため、テーブルをdivタグで囲み、そのdivと中のtableにこのプラグインで目印となるクラスを付与しています。

その目印を元に、親となるdivにはwhitespace-nowrapを、子となるtableタグにはoverflow-x-autoを適用することで、横スクロールを可能にしました。

コードはこちらです。

まとめ

今回は、rehypeプラグインだけで実装できる機能は全て実装することで、通信料とパフォーマンスを改善しました。

このブログ自体そもそも軽いので、これをしなくても実際のところあんまり変わらないのですが、気分的に要らない通信を省けるというのは良いのでやってよかったと思います。

ただ、rehypeプラグインを多用すると複雑性が増すような感じがするので、大きなプロジェクトでは素直にReactコンポーネントを用意して、アクセス時に置き換える実装にするべきかも知れません。