kb.erickguedes.com
Next.js: React Full-Stack

Performance e SEO

Aula 5 de 6

Performance

next/image

import Image from 'next/image';

// Otimização automática de imagens
export default function Profile() {
  return (
    <Image
      src="/profile.jpg"
      alt="Foto de perfil"
      width={400}
      height={400}
      priority               // Carregamento prioritário (above the fold)
      quality={85}           // Qualidade (1-100)
      placeholder="blur"     // Placeholder enquanto carrega
      blurDataURL="data:image/webp;base64,..." // Blur placeholder base64
      sizes="(max-width: 768px) 100vw, 400px"  // Responsivo
    />
  );
}

// Imagem externa (configurar no next.config.ts)
// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
  },
};

next/font

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

// Google Font (otimizada automaticamente)
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap'
});

const mono = Roboto_Mono({
  subsets: ['latin'],
  variable: '--font-mono'
});

// Fonte local
const geist = localFont({
  src: './fonts/GeistVF.woff2',
  variable: '--font-geist'
});

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="pt-BR" className={`${inter.variable} ${mono.variable}`}>
      <body style={{ fontFamily: 'var(--font-inter)' }}>
        {children}
      </body>
    </html>
  );
}

next/link (Prefetch)

import Link from 'next/link';

export default function Navbar() {
  return (
    <nav>
      <Link href="/" prefetch={true}>
        Home
      </Link>
      <Link href="/about" prefetch={true}>
        Sobre
      </Link>
      <Link href="/dashboard" prefetch={false}> {/* Sem prefetch */}
        Dashboard (autenticado)
      </Link>
    </nav>
  );
}

Streaming e Partial Pre-Rendering

// app/page.tsx
import { Suspense } from 'react';
import { ProductList } from '@/components/product-list';
import { ProductListSkeleton } from '@/components/skeletons';

export default function HomePage() {
  return (
    <div>
      <h1>Produtos</h1>

      {/* Streaming: renderiza enquanto carrega */}
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList />
      </Suspense>
    </div>
  );
}
// Partial Pre-Rendering (experimental)
// app/page.tsx
export const experimental_ppr = true;

export default function Page() {
  return (
    <div>
      {/* Shell estático (imediato) */}
      <header>Meu Site</header>

      {/* Conteúdo dinâmico (streamado) */}
      <Suspense fallback={<div>Carregando...</div>}>
        <DynamicContent />
      </Suspense>

      {/* Footer estático (imediato) */}
      <footer>© 2024</footer>
    </div>
  );
}

SEO

generateMetadata

// app/page.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    default: 'Meu Site',
    template: '%s | Meu Site'
  },
  description: 'Descrição principal do site',
  keywords: ['next.js', 'react', 'seo'],
  authors: [{ name: 'Autor' }],
  robots: {
    index: true,
    follow: true
  },
  openGraph: {
    type: 'website',
    locale: 'pt_BR',
    url: 'https://meusite.com',
    siteName: 'Meu Site',
    title: 'Meu Site',
    description: 'Descrição Open Graph',
    images: [
      {
        url: 'https://meusite.com/og-image.jpg',
        width: 1200,
        height: 630
      }
    ]
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Meu Site',
    description: 'Descrição Twitter Card'
  }
};

sitemap.ts

// app/sitemap.ts
import { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://meusite.com';

  // Buscar posts do banco
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  const blogPosts = posts.map((post: { slug: string; updatedAt: string }) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.8
  }));

  return [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 1
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.5
    },
    {
      url: `${baseUrl}/blog`,
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.9
    },
    ...blogPosts
  ];
}

robots.ts

// app/robots.ts
import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/admin/', '/api/', '/dashboard/']
    },
    sitemap: 'https://meusite.com/sitemap.xml'
  };
}

Structured Data (JSON-LD)

// components/json-ld.tsx
export function ArticleJsonLd({
  title,
  description,
  url,
  image,
  datePublished,
  author
}: {
  title: string;
  description: string;
  url: string;
  image: string;
  datePublished: string;
  author: string;
}) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: title,
    description,
    image,
    url,
    datePublished,
    author: {
      '@type': 'Person',
      name: author
    }
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  );
}

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  return (
    <article>
      <ArticleJsonLd
        title={post.title}
        description={post.excerpt}
        url={`https://meusite.com/blog/${post.slug}`}
        image={post.coverImage}
        datePublished={post.createdAt}
        author={post.author.name}
      />
      {/* ... */}
    </article>
  );
}

Core Web Vitals

// components/web-vitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    console.log(metric); // LCP, FID, CLS, FCP, TTFB

    // Enviar para analytics
    const body = JSON.stringify(metric);
    const url = '/api/vitals';

    if (navigator.sendBeacon) {
      navigator.sendBeacon(url, body);
    } else {
      fetch(url, { body, method: 'POST', keepalive: true });
    }
  });

  return null;
}

Lab: Exercício - Página Otimizada

// app/page.tsx
import Image from 'next/image';
import Link from 'next/link';
import { Suspense } from 'react';
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Home | Blog Tech',
  description: 'Blog sobre tecnologia e programação',
  openGraph: {
    title: 'Blog Tech',
    description: 'Artigos sobre desenvolvimento web'
  }
};

async function LatestPosts() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }
  }).then(r => r.json());

  return (
    <div className="grid gap-6">
      {posts.map((post: any) => (
        <article key={post.id}>
          <Link href={`/blog/${post.slug}`}>
            <Image
              src={post.coverImage}
              alt={post.title}
              width={800}
              height={400}
              sizes="(max-width: 768px) 100vw, 800px"
            />
            <h2>{post.title}</h2>
          </Link>
        </article>
      ))}
    </div>
  );
}

export default function HomePage() {
  return (
    <div>
      <h1>Últimos Posts</h1>
      <Suspense fallback={<div>Carregando posts...</div>}>
        <LatestPosts />
      </Suspense>
    </div>
  );
}
# Analisar performance
npm run build

# Testar Lighthouse no Chrome DevTools
# Verificar Core Web Vitals

next/image otimiza imagens automaticamente (WebP/AVIF, lazy loading, responsive). next/font elimina layout shift de fontes externas. Streaming com Suspense melhora TTFB e LCP. Dados estruturados (JSON-LD) melhoram rich snippets no Google.