6. Optimizing Performance and SEO

In the competitive world of web development, a fast-loading and search-engine-friendly application isn’t just a luxury—it’s a necessity. Next.js is built with performance and SEO in mind, offering powerful features out of the box. This chapter will guide you through leveraging these features, focusing on image optimization, font optimization, and robust metadata management to ensure your applications are both blazing fast and highly discoverable.

6.1 Image Optimization with next/image

Images often account for the largest portion of a webpage’s size, significantly impacting load times. Next.js provides the next/image component, a powerful tool that automatically optimizes images for performance and an improved user experience.

Key Features of next/image:

  • Automatic Image Optimization: Next.js automatically resizes, optimizes, and serves images in modern formats (like WebP or AVIF) when the browser supports them. This dramatically reduces file sizes without compromising visual quality.
  • Lazy Loading by Default: Images outside the viewport are not loaded until they are scrolled into view, reducing initial page load time.
  • Responsive Images: Automatically generates srcset attributes, serving different image sizes for various screen resolutions and device pixel ratios.
  • Prevention of Cumulative Layout Shift (CLS): Requires width and height props (or the fill prop) to reserve space, preventing layout shifts as images load.
  • External Image Sources: Supports optimization for images hosted on external domains with simple configuration.
  • Image Placeholders: Provides a placeholder prop to show a blurred placeholder while the image loads, enhancing perceived performance.

Basic Usage

To use next/image, simply import it and replace your standard <img> tags.

Example:

Let’s use next/image on our home page.

  1. Open src/app/page.tsx.

  2. Add an image from an external source (make sure to configure next.config.mjs for remote patterns if needed).

    // src/app/page.tsx
    import Link from 'next/link';
    import CustomButton from './components/CustomButton';
    import Image from 'next/image'; // Import the Image component
    
    export default function Home() {
      const handleButtonClick = (buttonName: string) => {
        alert(`${buttonName} button clicked!`);
      };
    
      return (
        <main className="min-h-screen flex flex-col items-center justify-center p-6 bg-gradient-to-br from-blue-50 to-indigo-100 text-gray-800">
          <Image
            src="https://images.unsplash.com/photo-1695669748983-63b7e75f4d1c?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
            alt="Modern desk setup with laptop and coffee"
            width={700} // Original width of the image
            height={400} // Original height of the image
            priority // Load this image with high priority (for LCP)
            className="rounded-lg shadow-xl mb-8"
            style={{ objectFit: 'cover' }} // Ensure image covers its area
          />
    
          <h1 className="text-5xl font-extrabold text-blue-800 mb-4">Welcome to Next.js!</h1>
          <p className="text-xl text-gray-700 mb-8 max-w-2xl text-center">
            This is the homepage of your Next.js application. <br />
            Let's learn and build amazing things!
          </p>
          <div className="flex space-x-4 mb-12">
            <Link href="/blog" className="text-blue-600 hover:text-blue-800 font-medium text-lg">
              View Blog Posts
            </Link>
            <Link href="/products" className="text-blue-600 hover:text-blue-800 font-medium text-lg">
              Explore Products
            </Link>
            <Link href="/users" className="text-blue-600 hover:text-blue-800 font-medium text-lg">
              See Users
            </Link>
          </div>
    
          <div className="mt-12 pt-8 border-t border-dashed border-gray-300 text-center">
            <h2 className="text-3xl font-bold text-gray-900 mb-6">Check out our custom buttons:</h2>
            <div className="flex justify-center gap-4">
              <CustomButton onClick={() => handleButtonClick('Primary')} variant="primary">
                Primary Action
              </CustomButton>
              <CustomButton onClick={() => handleButtonClick('Secondary')} variant="secondary" size="large">
                Large Secondary Action
              </CustomButton>
            </div>
          </div>
        </main>
      );
    }
    
  3. Configure next.config.mjs for remote images: If you’re using external images (like from Unsplash, as in the example), you must configure remotePatterns in next.config.mjs for security and optimization.

    // next.config.mjs
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      images: {
        remotePatterns: [
          {
            protocol: 'https',
            hostname: 'images.unsplash.com', // Allow images from Unsplash
            port: '',
            pathname: '**',
          },
          // Add other remote hosts here if needed
        ],
      },
    };
    
    export default nextConfig;
    

    You might need to restart your development server (npm run dev) after modifying next.config.mjs.

  4. Save the files and visit http://localhost:3000. Open your browser’s developer tools (Network tab) and observe the image. Next.js will likely have converted it to WebP (or AVIF) and generated multiple srcset entries, showing its optimization in action.

Important next/image Props:

  • src: The path to your image (local or remote URL).
  • alt: Essential for accessibility and SEO. Describes the image content.
  • width and height: Required for preventing CLS. Specifies the intrinsic dimensions.
  • priority: Set to true for images that are “above the fold” (visible on initial load) to prioritize loading and improve Largest Contentful Paint (LCP).
  • fill: When true, the image will fill its parent element (which must have position: relative, absolute, or fixed). Useful for fluid images where explicit width/height might be restrictive.
  • sizes: Provides a comma-separated list of media conditions and image slot widths. Crucial for generating optimal srcset when fill is used or for more complex responsive layouts.

Exercises/Mini-Challenges (Image Optimization):

  1. Add a placeholder to your homepage image:

    • Set placeholder="blur" and add a blurDataURL (you can generate a base64 blurhash or use a small, blurred version of the image for local static images).
    • For external images, you might need a library like plaiceholder (which you’d install) or manually provide a very small base64 image representation for testing. For this exercise, you can set placeholder="empty" to just see the effect of the space being reserved.
  2. Optimize an image in the Blog Page:

    • Find a suitable image (either local in /public or an external one) and add it to src/app/blog/page.tsx using next/image.
    • Ensure it has width, height, and alt attributes.
    • If it’s an image that appears early on the page, consider adding priority.

6.2 Font Optimization with next/font

Web fonts can also significantly impact performance, causing layout shifts (Flash of Unstyled Text - FOUT, or Flash of Invisible Text - FOIT). Next.js’s next/font module automatically optimizes fonts, eliminating external network requests and ensuring efficient loading.

Key Features of next/font:

  • Automatic Self-Hosting: Downloads font files at build time and serves them from your domain. No requests are sent to Google (or other providers) by the browser, improving privacy and performance.
  • Zero Layout Shift: Uses CSS size-adjust to maintain the layout during font loading, preventing CLS.
  • Optimized Loading: Reduces font file sizes through subsetting (only includes characters needed) and uses modern font formats (WOFF2).
  • Google Fonts & Local Fonts Support: Works seamlessly with both popular Google Fonts and your self-hosted local font files.

Using Google Fonts with next/font/google

  1. Open your root layout.tsx file (src/app/layout.tsx).

  2. Import the desired font from next/font/google. We are already using Inter, but let’s add Roboto_Mono for code blocks.

    // src/app/layout.tsx
    import type { Metadata } from "next";
    import { Inter, Roboto_Mono } from "next/font/google"; // Import Roboto_Mono
    import "./globals.css";
    
    const inter = Inter({ subsets: ["latin"], variable: '--font-inter' }); // Add variable
    const roboto_mono = Roboto_Mono({ subsets: ["latin"], variable: '--font-roboto-mono' }); // Define for code
    
    export const metadata: Metadata = {
      title: "My Next.js App",
      description: "Learning Next.js styling",
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        // Apply the font variables to the html element
        <html lang="en" className={`${inter.variable} ${roboto_mono.variable}`}>
          <body className={inter.className}>{children}</body> {/* Inter is the default body font */}
        </html>
      );
    }
    
  3. Configure Tailwind CSS to use the font variable: (If you are using Tailwind)

    • Open tailwind.config.ts.
    • Update the fontFamily extension.
    // tailwind.config.ts
    import type { Config } from 'tailwindcss';
    
    const config: Config = {
      content: [
        './pages/**/*.{js,ts,jsx,tsx,mdx}',
        './components/**/*.{js,ts,jsx,tsx,mdx}',
        './app/**/*.{js,ts,jsx,tsx,mdx}',
        './src/**/*.{js,ts,jsx,tsx,mdx}',
      ],
      theme: {
        extend: {
          backgroundImage: {
            'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
            'gradient-conic':
              'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
          },
          fontFamily: { // Add font family configuration
            sans: ['var(--font-inter)', 'sans-serif'],
            mono: ['var(--font-roboto-mono)', 'monospace'],
          },
        },
      },
      plugins: [],
    };
    export default config;
    
  4. Apply the font-mono class to elements where you want to use the monospace font (e.g., <pre> or <code> tags).

    • Let’s add a code example to src/app/page.tsx that uses the font-mono class:
    // src/app/page.tsx (inside the <main> element, perhaps near the bottom)
    // ... existing JSX ...
    
          <div className="mt-12 pt-8 border-t border-dashed border-gray-300 text-center">
            <h2 className="text-3xl font-bold text-gray-900 mb-6">Check out our custom buttons:</h2>
            <div className="flex justify-center gap-4">
              <CustomButton onClick={() => handleButtonClick('Primary')} variant="primary">
                Primary Action
              </CustomButton>
              <CustomButton onClick={() => handleButtonClick('Secondary')} variant="secondary" size="large">
                Large Secondary Action
              </CustomButton>
            </div>
          </div>
    
          <div className="mt-12 pt-8 border-t border-dashed border-gray-300">
            <h2 className="text-2xl font-bold text-gray-900 mb-4">Code Example:</h2>
            <pre className="bg-gray-800 text-white p-4 rounded-md overflow-x-auto font-mono text-left">
              {`// Example of using Roboto Mono for code
    

function greet(name: string) { console.log(`Hello, ${name}!`); }

greet(“World”); `}