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.