gzip または brotli 圧縮を有効にする#
一般的に、動静分離が静的リソースを CDN にアップロードしている場合、通常 CDN は上記の圧縮方式を有効にしています。
有効になっていない場合は、関連機能を有効にする必要があります。
画像圧縮#
できるだけ webp 形式の画像と圧縮画像を使用することで、画像のリクエストサイズを減らし、読み込み時間を短縮し、パフォーマンスを向上させることができます。
vercel プラットフォームにデプロイされているサービスの画像は、デフォルトで関連の最適化ロジックが有効になっています。
画像の遅延読み込み#
画像の遅延読み込みは、ウェブページのパフォーマンスを向上させる一般的な技術であり、画像がビューポートに入るまで読み込みを開始しないため、最初に過剰なリソースを読み込むのを避けることができます。
<Image loading="lazy" src={renderItem.img} alt="" fill />
dns-prefetch、preconnect メタタグの追加#
DNS プリフェッチ (dns-prefetch) とプリコネクト (preconnect) は、ブラウザが事前にドメイン解決と TCP ハンドシェイクを行うための技術です。
新しいページを読み込む際、ブラウザはドメインを解決し、TCP ハンドシェイクを確立するのに一定の時間を要します。dns-prefetch と preconnect メタタグを追加することで、これらの操作を事前に完了させ、ページの読み込み速度を大幅に向上させることができます。
<Head>
<link rel="dns-prefetch" href="//example.com" />
<link rel="preconnect" href="//example.com" />
</Head>
自部署 next サービスのカスタム画像ローダー最適化#
以下のコードは、画像の最適化機能を別のサービスに委託できるカスタム画像ローダーを示しています。
'use client'
import React, { ComponentProps } from 'react';
import Image from 'next/image';
export const CustomLoaderImg = (props: ComponentProps<typeof Image>) => {
return (
<Image
{...props}
loader={({ src, width, quality }) => {
// 使用可能な画像最適化サービスを確認した後に置き換え
return `https://optimize-img.com?url=${src}?w=${width}&q=${quality || 75}`;
}}
/>
);
};
一部のロジックコンポーネントとレンダリング内容をサーバーサイドレンダリングから独立させる#
ユーザーのインタラクション後にのみ表示される必要があるコンポーネントがあり、例えばスロットを予約した Modal コンポーネントのようなものです。このようなコンポーネントはサーバーサイドレンダリングの必要がなく、クライアントサイドレンダリングのみに実装を変更することを検討できます。同様に、時間に関連するコンテンツをレンダリングしている場合、サーバーとクライアントの時間にわずかな差異があるため、エラーが発生する可能性もあります。
これを以下の方法で実行できます:
- next が提供する dynamic を使用する
import dynamic from 'next/dynamic';
// クライアントサイドでのみ Modal コンポーネントを動的にインポートしてレンダリング
const DynamicModal = dynamic(
() => import('../components/Modal'),
{ ssr: false } // これによりサーバーサイドレンダリング(SSR)が無効になります
);
// クライアントサイドでのみ Time 関連コンポーネントを動的にインポートしてレンダリング
const DynamicTime = dynamic(
() => import('../components/Time'),
{ ssr: false } // これによりサーバーサイドレンダリング(SSR)が無効になります
);
- useEffect で動的にインポート
import React, { useState, useEffect } from 'react';
function YourComponent() {
const [DynamicComponent, setDynamicComponent] = useState(null);
useEffect(() => {
import('../components/Modal').then((Modal) => {
setDynamicComponent(() => Modal.default); // Modal.default に注意
});
}, []);
if (!DynamicComponent) return <p>読み込み中...</p>;
return <DynamicComponent />;
}
- ファーストフレーム後に最終コンテンツを実際にレンダリング
import React, { useState, useEffect } from 'react';
import Modal from '../components/Modal';
function YourComponent() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
// useEffect 内でフラグを設定して、現在クライアントサイドであることを示す
setIsClient(true);
}, []);
if (!isClient) return <p>読み込み中...</p>;
return <Modal />;
}
静的リソースを CDN にアップロードするプラグイン#
以下のコードは、パッケージされた静的ファイルを CDN にアップロードする方法を示しており、これによりサーバーの負荷を大幅に減少させることができます。また、CDN は通常、優れた地理的分布と読み込み最適化を備えているため、CDN を使用することでユーザーの読み込み体験をさらに向上させることができます。
/* eslint-disable react-func/max-lines-per-function */
/* eslint-disable max-depth */
const request = require('request');
function getCdnUrlPrefix(options) {
return `${options.packageVersion}/${options?.isEnvProduction ? 'prod' : 'dev'}`;
}
function UploadCDN(options) {
this.isEnvProduction = options?.isEnvProduction ?? false;
this.options = options;
}
function upload(options) {
return new Promise((resolve, reject) => {
request(options, function (error, response) {
if (error) reject(error);
resolve();
});
});
}
UploadCDN.prototype.apply = function (compiler) {
// eslint-disable-next-line react-func/max-lines-per-function
compiler.hooks.emit.tapAsync('UploadCDN', async (compilation, callback) => {
// formData リスト
let optionsMap = {};
let commonOptions = {
method: 'POST',
url: 'https://your-cdn-serverice/api/v1/files',
headers: {
module: this.options.module,
_dir: '/',
'Content-Type': 'application/x-www-form-urlencoded',
},
formData: {},
};
for (let fileName in compilation.assets) {
if (!fileName.endsWith('map')) {
const parts = fileName.split('/');
const filename = parts.pop();
const _dir = `${getCdnUrlPrefix(this.options)}/_next/${parts.join('/')}`;
const formData = {
value: compilation.assets[fileName].source(),
options: {
filename,
contentType: null,
},
};
if (!optionsMap[_dir]) {
optionsMap[_dir] = {
...JSON.parse(JSON.stringify(commonOptions)),
};
optionsMap[_dir].headers._dir = _dir;
}
optionsMap[_dir].formData[filename] = formData;
}
}
const optionsArr = Object.keys(optionsMap).map(key => optionsMap[key]);
optionsArr.forEach(async options => await upload(options));
callback();
});
};
module.exports = {
UploadCDN,
};
// next.config.js
// ...
assetPrefix: isProd ? `https://your-cdn-serverice/${yourModule}/${moduleName}/${packageVersion}/prod` : undefined,
webpack: (config, context) => {
isProd &&
config.plugins.push(
new UploadCDN({
module: moduleName,
packageVersion,
isEnvProduction: isProd,
}),
);
return config;
// ...