askill
performance-optimization

performance-optimizationSafety --Repository

Optimize Next.js portfolio for Core Web Vitals, bundle size, loading performance, and achieve 90+ Lighthouse scores.

6 stars
1.2k downloads
Updated 1/29/2026

Package Files

Loading files...
SKILL.md

Performance Optimization Mastery

Core Philosophy

Performance is not an afterthought—it's a feature. A slow portfolio loses visitors and creates a poor first impression. We optimize for Core Web Vitals because Google uses them for ranking, but more importantly, because users deserve fast experiences.

Core Web Vitals Targets

MetricTargetDescription
LCP (Largest Contentful Paint)< 2.5sHow fast the main content loads
FID (First Input Delay) / INP< 100msHow responsive to user input
CLS (Cumulative Layout Shift)< 0.1Visual stability (no jumping content)
FCP (First Contentful Paint)< 1.8sFirst visible content
TTFB (Time to First Byte)< 600msServer response time

Image Optimization

Next.js Image Component (Essential)

import Image from 'next/image';

// ✅ Local images (automatic optimization)
import heroImage from '@/public/images/hero.jpg';

<Image
  src={heroImage}
  alt="Hero section"
  placeholder="blur"          // Blur-up loading effect
  priority                    // Load immediately (above-the-fold)
  className="object-cover"
  sizes="100vw"              // Responsive sizing hints
/>

// ✅ Remote images
<Image
  src="https://example.com/image.jpg"
  alt="Project screenshot"
  width={1200}
  height={630}
  loading="lazy"             // Default behavior
  quality={85}               // Compression (default: 75)
  className="object-cover"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

Image Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    // Modern formats
    formats: ["image/avif", "image/webp"],

    // Remote patterns
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.unsplash.com",
      },
      {
        protocol: "https",
        hostname: "cdn.sanity.io",
      },
    ],

    // Device breakpoints
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],

    // Minimize processing (if using external CDN)
    // unoptimized: true,
  },
};

module.exports = nextConfig;

Image Best Practices

// 1. Always use sizes prop for responsive images
<Image
  src="/project.jpg"
  alt="Project"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

// 2. Use priority for above-the-fold images (hero, profile)
<Image src={heroImage} alt="Hero" priority />

// 3. Lazy load below-the-fold images (default)
<Image src={projectImage} alt="Project" loading="lazy" />

// 4. Prevent CLS with aspect ratio containers
<div className="relative aspect-video">
  <Image src="/video-thumb.jpg" alt="Video" fill className="object-cover" />
</div>

Font Optimization

Next.js Font System

// app/layout.tsx
import { Inter, Playfair_Display } from "next/font/google";
import localFont from "next/font/local";

// Google Fonts (self-hosted, optimized)
const inter = Inter({
  subsets: ["latin"],
  display: "swap", // Prevent FOIT (flash of invisible text)
  variable: "--font-inter",
  preload: true,
});

const playfair = Playfair_Display({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-playfair",
  weight: ["400", "700"], // Only load needed weights
});

// Local fonts (maximum control)
const calSans = localFont({
  src: "../public/fonts/CalSans-SemiBold.woff2",
  variable: "--font-cal",
  display: "swap",
  preload: true,
});

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html
      lang="en"
      className={`${inter.variable} ${playfair.variable} ${calSans.variable}`}
    >
      <body className="font-sans">{children}</body>
    </html>
  );
}

Font Subsetting

// Only load characters you need
const inter = Inter({
  subsets: ["latin"], // Not 'latin-ext' unless needed
  display: "swap",
});

// For icons/special characters, use local subset
const customFont = localFont({
  src: "../fonts/custom-subset.woff2", // Pre-subsetted
});

Bundle Optimization

Package Import Optimization

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    // Tree-shake package imports automatically
    optimizePackageImports: [
      "lucide-react",
      "framer-motion",
      "@radix-ui/react-icons",
      "date-fns",
      "lodash",
    ],
  },
};

Dynamic Imports (Code Splitting)

import dynamic from "next/dynamic";

// Heavy components loaded on demand
const HeavyChart = dynamic(() => import("@/components/heavy-chart"), {
  loading: () => <Skeleton className="h-96" />,
  ssr: false, // Client-only component
});

// Modal loaded when needed
const ContactModal = dynamic(() => import("@/components/contact-modal"));

// Animation components (reduce initial bundle)
const MotionDiv = dynamic(
  () => import("framer-motion").then((mod) => mod.motion.div),
  { ssr: false },
);

Analyze Bundle Size

// package.json
{
  "scripts": {
    "analyze": "ANALYZE=true next build"
  },
  "devDependencies": {
    "@next/bundle-analyzer": "^14.0.0"
  }
}
// next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});

module.exports = withBundleAnalyzer(nextConfig);

Import Optimization Patterns

// ❌ Bad: Imports entire library
import _ from "lodash";
_.debounce(fn, 300);

// ✅ Good: Tree-shakeable import
import debounce from "lodash/debounce";
debounce(fn, 300);

// ❌ Bad: All icons imported
import * as Icons from "lucide-react";

// ✅ Good: Only what you need
import { ArrowRight, Github, Mail } from "lucide-react";

// ❌ Bad: Moment.js (huge library)
import moment from "moment";

// ✅ Good: date-fns (tree-shakeable)
import { format, formatDistance } from "date-fns";

Rendering Strategies

Static Generation (Fastest)

// app/projects/page.tsx
// Statically generated at build time

export default async function ProjectsPage() {
  const projects = await getProjects(); // Fetched at build time

  return (
    <div>
      {projects.map((p) => (
        <ProjectCard key={p.id} project={p} />
      ))}
    </div>
  );
}

// Generate static paths
export async function generateStaticParams() {
  const projects = await getProjects();
  return projects.map((p) => ({ slug: p.slug }));
}

Incremental Static Regeneration (ISR)

// app/blog/page.tsx
// Revalidate every hour

export const revalidate = 3600; // seconds

export default async function BlogPage() {
  const posts = await getPosts();
  return <BlogList posts={posts} />;
}

// Per-fetch revalidation
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 3600 },
  });
  return res.json();
}

On-Demand Revalidation

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const { secret, path, tag } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
  }

  if (path) {
    revalidatePath(path);
  }

  if (tag) {
    revalidateTag(tag);
  }

  return NextResponse.json({ revalidated: true });
}

Loading Performance

Streaming with Suspense

// app/page.tsx
import { Suspense } from "react";
import { HeroSection } from "@/components/sections/hero";
import { ProjectsSkeleton } from "@/components/skeletons";

// Projects component fetches its own data
async function ProjectsSection() {
  const projects = await getProjects(); // This can be slow
  return <ProjectsList projects={projects} />;
}

export default function HomePage() {
  return (
    <main>
      {/* Hero loads immediately */}
      <HeroSection />

      {/* Projects stream in when ready */}
      <Suspense fallback={<ProjectsSkeleton />}>
        <ProjectsSection />
      </Suspense>
    </main>
  );
}

Loading States

// app/projects/loading.tsx
import { Skeleton } from "@/components/ui/skeleton";

export default function ProjectsLoading() {
  return (
    <div className="container py-12">
      {/* Header skeleton */}
      <div className="mx-auto max-w-2xl text-center">
        <Skeleton className="mx-auto h-4 w-24" />
        <Skeleton className="mx-auto mt-2 h-10 w-80" />
        <Skeleton className="mx-auto mt-4 h-6 w-96" />
      </div>

      {/* Grid skeleton */}
      <div className="mt-16 grid gap-8 md:grid-cols-2 lg:grid-cols-3">
        {Array.from({ length: 6 }).map((_, i) => (
          <div key={i} className="rounded-2xl border border-zinc-800 p-6">
            <Skeleton className="aspect-video rounded-lg" />
            <Skeleton className="mt-4 h-6 w-3/4" />
            <Skeleton className="mt-2 h-4 w-full" />
            <Skeleton className="mt-2 h-4 w-2/3" />
          </div>
        ))}
      </div>
    </div>
  );
}

Skeleton Component

// components/ui/skeleton.tsx
import { cn } from "@/lib/utils";

interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}

export function Skeleton({ className, ...props }: SkeletonProps) {
  return (
    <div
      className={cn("animate-pulse rounded-md bg-zinc-800", className)}
      {...props}
    />
  );
}

Preloading & Prefetching

Link Prefetching

import Link from 'next/link';

// Automatic prefetch on viewport enter (production)
<Link href="/projects">Projects</Link>

// Disable prefetch for less important links
<Link href="/privacy" prefetch={false}>Privacy Policy</Link>

Resource Hints

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        {/* Preconnect to external origins */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link
          rel="preconnect"
          href="https://fonts.gstatic.com"
          crossOrigin="anonymous"
        />

        {/* DNS prefetch for analytics */}
        <link rel="dns-prefetch" href="https://www.googletagmanager.com" />

        {/* Preload critical assets */}
        <link
          rel="preload"
          href="/fonts/CalSans-SemiBold.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Animation Performance

GPU-Accelerated Properties

// ✅ Good: GPU-accelerated (transform, opacity)
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
/>

// ❌ Bad: Triggers layout (width, height, left, top)
<motion.div
  initial={{ width: 0 }}
  animate={{ width: 200 }}
/>

Reduce Motion for Accessibility

"use client";

import { useReducedMotion } from "framer-motion";

export function AnimatedComponent() {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={shouldReduceMotion ? false : { opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5 }}
    >
      Content
    </motion.div>
  );
}

CSS-Based Animations (Lower Overhead)

// For simple animations, CSS is lighter than Framer Motion

// tailwind.config.ts
animation: {
  'fade-in': 'fade-in 0.5s ease-out forwards',
  'slide-up': 'slide-up 0.5s ease-out forwards',
},
keyframes: {
  'fade-in': {
    from: { opacity: '0' },
    to: { opacity: '1' },
  },
  'slide-up': {
    from: { opacity: '0', transform: 'translateY(20px)' },
    to: { opacity: '1', transform: 'translateY(0)' },
  },
},

// Usage
<div className="animate-fade-in">Content</div>

Caching Strategies

Fetch Caching

// Cache indefinitely (static data)
const data = await fetch("https://api.example.com/static", {
  cache: "force-cache",
});

// No caching (always fresh)
const data = await fetch("https://api.example.com/realtime", {
  cache: "no-store",
});

// Revalidate after time
const data = await fetch("https://api.example.com/posts", {
  next: { revalidate: 3600 },
});

// Tag-based revalidation
const data = await fetch("https://api.example.com/posts", {
  next: { tags: ["posts"] },
});

React Cache

import { cache } from "react";

// Deduplicate calls within same request
export const getUser = cache(async (id: string) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

// Both components call getUser - only ONE fetch happens
async function UserHeader({ userId }: { userId: string }) {
  const user = await getUser(userId);
  return <h1>{user.name}</h1>;
}

async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId);
  return <p>{user.bio}</p>;
}

unstable_cache (Data Cache)

import { unstable_cache } from "next/cache";

const getCachedPosts = unstable_cache(
  async () => {
    return db.post.findMany();
  },
  ["posts"], // Cache key
  {
    revalidate: 3600, // Revalidate every hour
    tags: ["posts"], // Tag for on-demand revalidation
  },
);

// Usage
const posts = await getCachedPosts();

Third-Party Scripts

Analytics (Vercel)

// app/layout.tsx
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

External Scripts

import Script from 'next/script';

// Load after page interactive
<Script
  src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
  strategy="afterInteractive"
/>

// Load when browser is idle
<Script
  src="https://widget.example.com/embed.js"
  strategy="lazyOnload"
/>

// Worker thread (experimental)
<Script
  src="https://heavy-analytics.js"
  strategy="worker"
/>

Script Loading Strategies

StrategyWhenUse Case
beforeInteractiveBefore hydrationCritical polyfills
afterInteractiveAfter hydrationAnalytics, chat widgets
lazyOnloadBrowser idleNon-critical embeds
workerWeb WorkerHeavy analytics (experimental)

Monitoring & Debugging

Web Vitals Reporting

// app/layout.tsx
export function reportWebVitals(metric: {
  id: string;
  name: string;
  startTime: number;
  value: number;
  label: "web-vital" | "custom";
}) {
  // Send to analytics
  if (metric.label === "web-vital") {
    fetch("/api/analytics", {
      method: "POST",
      body: JSON.stringify({
        name: metric.name,
        value: metric.value,
        id: metric.id,
      }),
    });
  }
}

Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: push

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v10
        with:
          urls: |
            https://yourportfolio.com/
            https://yourportfolio.com/projects
          uploadArtifacts: true
          temporaryPublicStorage: true

Performance Checklist

Pre-Launch

  • All images use <Image /> component
  • Fonts use next/font
  • Dynamic imports for heavy components
  • Bundle analyzed and optimized
  • No layout shifts (CLS < 0.1)

Images

  • All images have explicit dimensions or use fill
  • Above-the-fold images have priority
  • Below-the-fold images use lazy loading
  • sizes prop set for responsive images
  • WebP/AVIF formats enabled

JavaScript

  • Tree shaking enabled
  • Code splitting implemented
  • Third-party scripts use correct strategy
  • No render-blocking scripts

Caching

  • Static pages use ISR where appropriate
  • API routes cached correctly
  • Fonts cached with long max-age
  • Static assets have immutable cache headers

Monitoring

  • Vercel Analytics enabled
  • SpeedInsights enabled
  • Core Web Vitals tracked
  • Error boundaries in place

Quick Wins

  1. Add priority to hero image - Immediate LCP improvement
  2. Use next/font - Eliminates FOUT/FOIT
  3. Enable optimizePackageImports - Smaller bundles
  4. Add Suspense boundaries - Better perceived performance
  5. Use ISR - Faster TTFB than SSR
  6. Lazy load modals - Reduce initial bundle
  7. Skeleton loaders - Better UX than spinners

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

AI review pending.

Metadata

Licenseunknown
Version1.0.0
Updated1/29/2026
PublisherJaivishChauhan

Tags

apici-cdgithubgithub-actionsobservabilitysecurity