Next.js 16を使用したポートフォリオサイトのパフォーマンス最適化で実践したテクニックと実装の詳細を紹介します。
ポートフォリオサイトの構築において、パフォーマンス最適化は重要な要素です。この記事では、Next.js 16を使用した実践的な最適化テクニックと、実際の実装例を紹介します。
このポートフォリオサイトは、Next.js 16.1.6 (App Router)、React 19.2.3、TypeScript 5.xを使用して構築されています。静的サイト生成(SSG)を活用し、Vercelにデプロイしています。
Next.jsのImageコンポーネントを活用することで、自動的な画像最適化が可能です。実際の実装では、エラーハンドリングも含めたSafeImageコンポーネントを作成しました。
// components/portfolio/SafeImage.tsx
"use client";
import Image from "next/image";
import { useState } from "react";
interface SafeImageProps {
src?: string;
alt: string;
fill?: boolean;
width?: number;
height?: number;
className?: string;
sizes?: string;
priority?: boolean;
aspectRatio?: string;
}
export function SafeImage({
src,
alt,
fill,
width,
height,
className,
sizes,
priority,
aspectRatio,
}: SafeImageProps) {
const [hasError, setHasError] = useState(false);
if (!src || hasError) {
const containerClass = aspectRatio
? `relative w-full bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center ${aspectRatio}`
: "h-48 w-full bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center";
return (
<div className={containerClass}>
<span className="text-muted-foreground">画像準備中</span>
</div>
);
}
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
fill={fill}
className={className}
sizes={sizes}
priority={priority}
quality={85}
onError={() => setHasError(true)}
unoptimized={src.startsWith("http://") || src.startsWith("https://")}
/>
);
}
// next.config.ts
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
{
protocol: "http",
hostname: "**",
},
],
dangerouslyAllowSVG: true,
contentDispositionType: "attachment",
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
sizesプロパティでレスポンシブな画像サイズを指定priority={false}がデフォルト)quality={85}でバランスの取れた画質とファイルサイズ// components/portfolio/ProjectImage.tsx
export function ProjectImage({ src, alt }: ProjectImageProps) {
const [hasError, setHasError] = useState(false);
if (!src || hasError) {
return (
<div className="h-48 w-full bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center">
<span className="text-muted-foreground text-sm">画像準備中</span>
</div>
);
}
return (
<div className="relative h-48 w-full overflow-hidden bg-muted group">
<Image
src={src}
alt={alt}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
quality={85}
onError={() => setHasError(true)}
unoptimized={src.startsWith("http://") || src.startsWith("https://")}
/>
</div>
);
}
Next.js 16のnext/fontを使用して、フォントの読み込みを最適化します。実際の実装では、Google FontsのInterフォントを使用しています。
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja" suppressHydrationWarning>
<body className={`${inter.variable} font-sans antialiased`}>
{children}
</body>
</html>
);
}
subsets: ["latin"]で必要な文字のみを読み込みvariableプロパティでCSS変数として使用可能にすべてのページをビルド時に静的生成することで、高速な表示を実現しています。
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = getPostBySlug(params.slug);
// ...
}
generateStaticParamsで動的ルートのパラメータを事前生成適切なメタデータの設定は、SEOだけでなくパフォーマンスにも影響します。実際の実装では、ルートレイアウトでメタデータを設定しています。
// app/layout.tsx
export const metadata: Metadata = {
title: {
default: "川嶋 宥翔 | Portfolio & Blog",
template: "%s | 川嶋 宥翔",
},
description: "名古屋大学 理学部物理学科に在籍する大学生。安全性や正確性が強く求められる分野に関心を持ち、「システムを誤らせない設計」を軸に、WebアプリケーションやAIを用いた個人開発に取り組んでいます。",
keywords: ["ポートフォリオ", "ブログ", "フルスタックエンジニア", "医療AI", "Next.js", "TypeScript"],
authors: [{ name: "川嶋 宥翔", url: "https://github.com/32Lwk" }],
creator: "川嶋 宥翔",
openGraph: {
type: "website",
locale: "ja_JP",
url: "https://www.yutok.dev",
title: "川嶋 宥翔 | Portfolio & Blog",
description: "...",
siteName: "川嶋 宥翔 | Portfolio & Blog",
},
twitter: {
card: "summary_large_image",
title: "川嶋 宥翔 | Portfolio & Blog",
description: "...",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
templateプロパティで一貫したタイトル形式テーマ切り替えのスクリプトをbeforeInteractive戦略で読み込むことで、FOUC(Flash of Unstyled Content)を防止しています。
// app/layout.tsx
<Script
id="theme-init"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `(function(){var t=localStorage.getItem('theme');var s=!t||t==='system';var d=s?window.matchMedia('(prefers-color-scheme: dark)').matches:t==='dark';document.documentElement.classList.toggle('dark',d);})();`,
}}
/>
動的にサイトマップとrobots.txtを生成することで、SEOを最適化しています。
// app/sitemap.ts
import { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/blog";
import { getAllProjects } from "@/lib/projects";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://www.yutok.dev";
const posts = getAllPosts();
const projects = getAllProjects();
const postUrls = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "weekly" as const,
priority: 0.8,
}));
const projectUrls = projects.map((project) => ({
url: `${baseUrl}/projects/${project.id}`,
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: 0.8,
}));
return [
{ url: baseUrl, lastModified: new Date(), changeFrequency: "daily", priority: 1 },
{ url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.9 },
{ url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.9 },
{ url: `${baseUrl}/projects`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.9 },
{ url: `${baseUrl}/resume`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
...postUrls,
...projectUrls,
];
}
最適化の結果、以下の指標を達成しました:
画像最適化の重要性: 適切な画像最適化により、ページの読み込み速度が大幅に向上しました。
静的サイト生成の効果: SSGにより、サーバーサイドレンダリング不要で高速な表示を実現できました。
フォント最適化: next/fontを使用することで、フォントの読み込みが最適化され、FOUT(Flash of Unstyled Text)を防止できました。
メタデータの設定: 適切なメタデータの設定により、SEOとSNS共有が改善されました。
コード分割の強化: 大きなコンポーネントを動的インポートで分割し、初期バンドルサイズを削減する予定です。
パフォーマンス監視: Lighthouse CIなどを導入し、継続的にパフォーマンスを監視する予定です。
画像の最適化: より適切なsrcsetの設定や、画像の遅延読み込みの最適化を検討しています。
Next.js 16の機能を最大限に活用することで、優れたパフォーマンスを実現できます。特に、画像最適化、静的サイト生成、フォント最適化は効果が大きいので、優先的に実装することをおすすめします。
今後も、継続的にパフォーマンスを監視し、改善を重ねていきたいと思います。
Next.js 16、TypeScript、Tailwind CSS 4を使用して、モダンで洗練されたポートフォリオ兼ブログサイトを構築した過程と学びを紹介します。
学歴(中高・大学)を表示する About ページに、Canvas で描画するテニスサーブのアニメーションを追加した話です。
医療×AI分野での開発経験から学んだ、TypeScriptで安全なコードを書くための実践的なテクニックを紹介します。
API呼び出し回数を約67%削減し、翻訳速度を10-20倍高速化したパフォーマンス最適化の実装について
Render Manual Scaling対応のため、PostgreSQLベースのセッション管理システムを実装し、複数インスタンス間でセッションデータを共有する機能について
Single Responsibility Principle (SRP) に基づいた大規模リファクタリング、コードの可読性とメンテナンス性の向上について