💴

自作コンポーネントでコスト削減とサーバ負荷低減を実現した話

に公開

はじめに

フリーランスエンジニアのいのっちです。
株式会社イノベーションが運営しているITトレンドの開発(+α)に業務委託で参画しています。
ITトレンドのフロントをNext.jsにリプレイスしていく中で行った「ちょっとした改善」についてお話ししたいと思います。

何をしたか

  1. Vercelでの画像最適化回数および不要なキャッシュの抑制
  2. next/linkによるプリフェッチの抑制

1. Vercelでの画像最適化回数および不要なキャッシュの抑制

背景

Next.jsにおける画像の表示は、一般的にはImageタグ(next/image)を使用します。
画像の最適化や画像キャッシュなどを自動で行ってくれるというメリットがあります。

しかし、ITトレンドでは掲載している製品や記事などが多数存在するため、使用している画像が全て最適化の対象となってしまうとVercelの契約プランにおける最適化回数の上限を軽々と超過してしまう可能性が高いという懸念がありました。
また、Next.jsにリプレイスする以前から画像はCDN経由で取得しており、キャッシュが二重になり無駄なリソースを消費しているという指摘もありました。

これらの懸念を解消するため、既にCDNで管理されている画像についてはImageタグを使用せず、imgタグで表示する方針をとりました。
また、ただimgタグを使用するというルールにしてしまうと開発担当者ごとに実装の統一感が無くなったり、パラメータ不備によるビルドエラーやレイアウトシフトによるSEO評価の低減などの問題が発生する可能性があります。

そこで、imgタグを使用しつついくつかのパラメータを必須化した画像表示用コンポーネントを作成することにしました。

作成したコンポーネント

こんなのです。

import React from 'react';

type Props = Omit<React.ComponentProps<'img'>, 'src' | 'width' | 'height' | 'alt'> & {
  src: string;
  width: number | string;
  height: number | string;
  alt: string;
};

const CdnImg = ({ alt, loading = 'lazy', ...props }: Props) => {
  return <img alt={alt} loading={loading} {...props} />;
};

export default CdnImg;

ポイント

  1. 画像の出力はimgタグで行う
    Imageタグを使用することによるキャッシュ二重化を抑止するため
  2. imgタグの属性のうち、「src」「width」「height」「alt」は入力必須にする
    widthとheightは必須とすることでレイアウトシフトを抑止するため
    srcは言わずもがな、altはアクセシビリティとSEO評価の向上のため
  3. loadingのデフォルト設定を'lazy'にする
    Imageタグを使用したときと同様に遅延読み込みにして、画面表示までの時間を短くするため

効果

本対応前には1ヶ月の画像最適化回数がプラン上限に迫る勢いだったのが、対応後にはプラン上限の20%程度までに抑えられ、上位プラン契約による費用拡大を防ぐことができました

本対応前後のSource Image Optivizations
(キャッシュについては特に検証できておりません・・・)

2. next/linkによるプリフェッチの抑制

背景

Next.jsで構築されたページ間の遷移は、next/linkを使用するのが一般的です。
クライアントサイドでの遷移となるため遷移が高速であったり、SWRによるキャッシュが有効なためページ表示が高速であったりとメリットは多いです。
また、プリフェッチによりさらにページ遷移が高速になるという点もメリットとして挙げられます。

しかし、ITトレンドにおいてはページ内に配置されているリンクもたくさんあります。
next/linkのプリフェッチは通常「ビューポート内に入ったリンク先を自動的にプリフェッチする」という仕様のため、プリフェッチによりAPIサーバに負荷がかかり、ページ表示が不安定になるといったトラブルにもつながりかねません。

そのため、Next.jsにリプレイス済みのページへのリンクは「プリフェッチをOFFにしたLinkタグ」を使用する方針としました。

作成したコンポーネント

こんなのです。

import Link, { LinkProps } from 'next/link';
import { AnchorHTMLAttributes, Attributes, FC, ReactNode } from 'react';

type Props = Omit<LinkProps, 'prefetch'> &
  Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> &
  Attributes & { children: ReactNode };

const NpLink: FC<Props> = ({ children, ...props }: Props) => {
  return (
    <Link prefetch={false} {...props}>
      {children}
    </Link>
  );
};
export default NpLink;

ポイント

  1. prefetchはfalse固定
    プリフェッチを抑止するため
  2. その他プロパティについては、使用箇所に応じて最低限エラーとならないよう型を結合
    classNameとかkeyとか子要素が指定できれば大体OK

効果

本対応前には監視システムからの負荷上昇アラートが頻発していましたが、対応後にはアラートの頻度が下がり、負荷軽減に一定の効果があったものと思われます。
(負荷軽減対策として他にもいろいろ実施していたため、確実に効果があったと言い切れる状況ではありませんが・・・)

改善してみて

(あえて「この程度」と書きますが)この程度の実装でもそれなりの改善効果が得られたという成功体験ができたのは大きいです。
コーディング規約により記述を強制するのではなく、使用するコンポーネントを強制することでルールを一律に適用できるという事例ができたことで、ルール制定時の選択肢が増えたということも良かったです。
ESLintなどで制約をかけることで、さらにコーディングのブレを抑える方向に持っていければなお良いかなと思いました。

ただ、それぞれの効果の項にも書いていますが、あまり定量的な効果測定ができていなかったのが残念ではあります。
金額みたいにわかりやすい効果があればもっとドヤれたんですが・・・残念です。

引き続き、より良いものを生み出していくため改善に努めていきます。

最後まで読んでいただきありがとうございました。

株式会社イノベーション Tech Blog

Discussion