iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Vibe Coding

Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地系列 第 24

Day 24|Next.js效能優化與PWA支援:Lighthouse滿分挑戰

  • 分享至 

  • xImage
  •  

在Day23中,我們成功完成了全站Dashboard數據儀表板,整合了全站KPI指標與Recharts雙維度視覺化圖表。

今天(Day24),我們將聚焦於產品邁向Production生產環境前最關鍵的一環——效能優化、SEO動態社交卡片與PWA離線支援!

我們將針對VibePulse進行全方位的效能調優與PWA改造,目標是在GoogleLighthouse測試中拿下綠標乃至滿分:

CodeSplitting與DynamicImports:將龐大的圖表庫(Recharts)與Markdown編輯器進行動態載入,大幅降低首屏InitialBundleSize。

DynamicOGMetadata(@vercel/og):利用Next.jsImageResponse自動為每個技能頁面生成獨一無二的動態社交分享卡片(OpenGraphImage)。

PWA漸進式Web應用(next-pwa):配置WebAppManifest與ServiceWorker,讓VibePulse支援手機/桌面端安裝至主畫面與離線快取。

1.步驟1:動態載入重型組件(DynamicImports)
像Recharts或ByteMD這類包含大量DOM操作與Math運算的第三方套件,若在首屏直接同步import,會大幅拉長FirstContentfulPaint(FCP)與TimetoInteractive(TTI)。

我們利用Next.js的next/dynamic進行懶載入(LazyLoading)與SSR關閉:

// components/dashboard/DynamicCharts.tsx
'use client';

import dynamic from 'next/dynamic';
import { Skeleton } from '@/components/ui/Skeleton';

// 動態載入重型圖表組件,關閉 SSR 避免 Recharts 水合 (Hydration) 不一致
export const DynamicCategoryRadarChart = dynamic(
  () => import('./CategoryRadarChart').then((mod) => mod.CategoryRadarChart),
  {
    ssr: false,
    loading: () => <Skeleton className="w-full h-80 rounded-2xl bg-slate-900/60" />,
  }
);

export const DynamicPracticeTrendChart = dynamic(
  () => import('./PracticeTrendChart').then((mod) => mod.PracticeTrendChart),
  {
    ssr: false,
    loading: () => <Skeleton className="w-full h-80 rounded-2xl bg-slate-900/60" />,
  }
);

在app/dashboard/page.tsx中直接替換為DynamicCategoryRadarChart,即可立刻從主Bundle中抽離數百KB的JavaScript!

2.步驟2:動態OpenGraph社交卡片生成(@vercel/og)
當使用者分享技能連結(如/skills/[id])至Line、Discord或Twitter時,高質感的動態封面卡片能大幅提升點擊率。

Next.js內建的ImageResponse允許我們使用JSX/HTML/TailwindCSS語法在EdgeRuntime即時渲染PNG圖片:

在技能詳細頁目錄下新增app/skills/[id]/opengraph-image.tsx:

// app/skills/[id]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { prisma } from '@/lib/prisma';

export const runtime = 'edge';
export const alt = 'VibePulse 技能學習卡片';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image({ params }: { params: { id: string } }) {
  const skill = await prisma.skill.findUnique({
    where: { id: params.id },
    select: { title: true, category: true, proficiency: true },
  });

  const title = skill?.title || '未知技能';
  const category = skill?.category || 'General';
  const proficiency = skill?.proficiency || 0;

  return new ImageResponse(
    (
      <div
        style={{
          background: 'linear-gradient(to bottom right, #0f172a, #020617)',
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'flex-start',
          justifyContent: 'space-between',
          padding: '80px',
          color: '#f8fafc',
          fontFamily: 'sans-serif',
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
          <div
            style={{
              padding: '8px 20px',
              borderRadius: '9999px',
              background: 'rgba(6, 182, 212, 0.2)',
              border: '1px solid rgba(6, 182, 212, 0.4)',
              color: '#22d3ee',
              fontSize: '24px',
              fontWeight: 'bold',
            }}
          >
            {category}
          </div>
          <span style={{ color: '#64748b', fontSize: '24px' }}>VibePulse Skill Share</span>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
          <h1 style={{ fontSize: '64px', fontWeight: 900, color: '#f8fafc', margin: 0 }}>
            {title}
          </h1>
          <p style={{ fontSize: '28px', color: '#94a3b8', margin: 0 }}>
            當前掌握度進度:{proficiency}%
          </p>
        </div>

        {/* 進度條 */}
        <div
          style={{
            width: '100%',
            height: '16px',
            background: '#1e293b',
            borderRadius: '8px',
            overflow: 'hidden',
            display: 'flex',
          }}
        >
          <div
            style={{
              width: `${proficiency}%`,
              height: '100%',
              background: 'linear-gradient(to right, #06b6d4, #3b82f6)',
            }}
          />
        </div>
      </div>
    ),
    { ...size }
  );
}

3.步驟3:配置PWA(ProgressiveWebApp)
讓VibePulse能安裝至手機桌面,並在無網路環境下展示快取頁面。
A.安裝@ducanh2912/next-pwa

npm install @ducanh2912/next-pwa

B.設定next.config.mjs

// next.config.mjs
import withPWAInit from '@ducanh2912/next-pwa';

const withPWA = withPWAInit({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development', // 開發環境關閉 Service Worker
  register: true,
  skipWaiting: true,
});

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};

export default withPWA(nextConfig);

C.建立public/manifest.json

{
  "name": "VibePulse 技能圖譜與學習追蹤",
  "short_name": "VibePulse",
  "description": "個人化技能圖譜、Markdown 學習筆記與數據可視化打卡工具",
  "start_url": "/dashboard",
  "display": "standalone",
  "background_color": "#020617",
  "theme_color": "#06b6d4",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

D.在app/layout.tsx中注入Manifest與ThemeColor

// app/layout.tsx
import type { Metadata, Viewport } from 'next';

export const metadata: Metadata = {
  title: 'VibePulse - 技能圖譜與學習追蹤',
  description: '個人化技能圖譜與數據打卡工具',
  manifest: '/manifest.json',
  appleWebApp: {
    capable: true,
    statusBarStyle: 'black-translucent',
    title: 'VibePulse',
  },
};

export const viewport: Viewport = {
  themeColor: '#06b6d4',
  width: 'device-width',
  initialScale: 1,
  maximumScale: 1,
};

今天我們完成了VibePulse生產環境級別的最後優化:

CodeSplitting瘦身:使用next/dynamic隔離Recharts與大型組件,減少JS首屏下載體積。

動態社交分享卡片:整合@vercel/ogEdgeAPI,每次分享技能時自動即時生成包含實時熟練度與類別的PNG視覺圖卡。

PWA支援:配置WebAppManifest與ServiceWorker快取,支援手機/桌面獨立視窗安裝與離線載入。


上一篇
Day 23|全站Dashboard數據儀表板總結統計KPI與Recharts綜合圖表
下一篇
Day 25|Vercel自動化CI/CD部署與Neon PostgreSQL上雲
系列文
Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言