İçeriğe geç
Kamer StudioKamer Studio
Tüm yazılar
Teknik notlarYayın tarihi: 3 Eylül 20266 dk okuma

Next.js'te 95+ Lighthouse skoru: pratik kontrol listesi

Font yükleme, görsel boyutlandırma, üçüncü taraf script'ler ve layout shift — dört başlıkta, uygulanabilir bir kontrol listesi.

Lighthouse skorunu 95'in üstüne çıkarmak tek bir büyük değişiklikle olmuyor; dört ayrı kaynaktan gelen küçük kayıpların toplamı düşük skoru oluşturuyor. Bu yazıda dördünü de tek tek ele alıyorum: font yükleme, görsel boyutlandırma, üçüncü taraf script'ler ve layout shift. Sonda da nereden ölçüleceğini yazıyorum, çünkü tahminle optimizasyon yapmak zaman kaybı.

Bu dördü rastgele seçilmiş değil; Lighthouse'un puanladığı Core Web Vitals metriklerinin üçüne doğrudan denk düşüyor. Font ve görsel optimizasyonu Largest Contentful Paint'i (LCP — ekrandaki en büyük içeriğin ne kadar hızlı çizildiği) etkiliyor. Üçüncü taraf script'ler ana iş parçacığını meşgul ederek etkileşim gecikmesini (INP) büyütüyor. Görsel boyutlandırma ve yer ayırma ise Cumulative Layout Shift'i (CLS — sayfanın yüklenirken ne kadar "zıpladığı") belirliyor. Bu üç metriği tek tek iyileştirmek, toplam skoru da yukarı çeker.

Font yükleme: self-hosting ve next/font

Google Fonts'u bir <link> etiketiyle çağırmak bir ağ isteği daha demektir; tarayıcı önce sizin sunucunuza, sonra Google'ın sunucusuna bağlanır. next/font bunu build sırasında indirip kendi sunucunuzdan servis eder — üçüncü taraf isteği tamamen ortadan kalkar:

// lib/fonts.ts
import { Inter } from 'next/font/google'

export const inter = Inter({
  subsets: ['latin', 'latin-ext'],
  display: 'swap',
  variable: '--font-inter',
})

display: 'swap' metnin görünmez kalmasını (FOIT) engeller; sistem fontuyla başlar, gerçek font hazır olunca değiştirir. Buradaki asıl kazanç görünmez bir tarafta: next/font seçtiğiniz fontun metriklerine göre otomatik bir fallback font ayarlar (size-adjust, ascent-override gibi CSS özellikleriyle), böylece font değiştiğinde satır yükseklikleri ve kelime genişlikleri neredeyse aynı kalır. Bu, font yüklenince sayfanın aşağı kayması sorununu — layout shift'in en yaygın kaynaklarından biri — kaynağında çözer.

Birden fazla font ağırlığı (regular, medium, bold) kullanıyorsanız weight parametresiyle bunları açıkça listeleyin — next/font/google, statik (variable olmayan) bir fontta yalnızca belirttiğiniz ağırlıkları indirir; kullanmadığınız her ağırlık indirilmeyen, dolayısıyla boşa harcanmayan bir dosyadır. Aynı şekilde subsets parametresini gerçekten ihtiyacınız olan karakter setleriyle sınırlı tutun — Türkçe bir site için latin tek başına yetmez, latin-ext de gerekir, ama kullanmayacağınız bir alfabenin karakterlerini indirmenize gerek yok.

Fontu variable ile bir CSS değişkenine bağlayıp kök elemente uygulamak, her bileşende ayrı ayrı import etmenizi gerektirmez:

// app/layout.tsx
import { inter } from '@/lib/fonts'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}

Görsel boyutlandırma: next/image ve açık boyutlar

next/image otomatik olarak boyut optimize eder, ama asıl kritik olan width ve height (ya da fill) vermek. Next, bu değerlerden görselin en-boy oranını hesaplar ve tarayıcıya görsel yüklenmeden önce ne kadar yer ayıracağını söyler:

import Image from 'next/image'

<Image
  src="/ekip.jpg"
  alt="Ekip fotoğrafı"
  width={1200}
  height={800}
  sizes="(min-width: 768px) 50vw, 100vw"
/>

Boyut vermeden bırakılan bir <img>, tarayıcı görselin gerçek boyutunu öğrenene kadar sıfır yükseklikte davranır; görsel indiğinde altındaki içerik aşağı iter. Bu, Lighthouse'un Cumulative Layout Shift (CLS) metriğini doğrudan bozan bir davranış. sizes özniteliği de aynı derecede önemli: tarayıcıya ekran genişliğine göre hangi boyuttaki görseli indireceğini söyler, mobilde masaüstü boyutunda bir dosya indirmesini önler.

Sayfa yüklendiğinde ilk ekranda görünen bir görsel varsa (örneğin hero görseli), priority ekleyin; bu, Next'e görseli tembel yüklemek yerine hemen indirmesini söyler ve genelde Largest Contentful Paint (LCP) metriğini iyileştirir. next/image'ın varsayılan davranışı, ilk ekranın dışındaki her görseli tarayıcı görünüme yaklaşana kadar indirmemek (lazy loading); priority bu davranışın istisnasıdır ve yalnızca gerçekten ilk ekranda görünen, tek bir görsel için kullanılmalı — her görsele priority eklemek hepsini eşit derecede öncelikli, yani hiçbirini öncelikli yapmaz.

next/image'ın bir diğer faydası, tarayıcının desteklediği en verimli formatı (AVIF ya da WebP) otomatik olarak sunması: kaynak dosyanız JPEG olsa bile, destekleyen bir tarayıcıya daha küçük boyutlu bir format servis edilir, siz elle bir dönüştürme yapmazsınız.

Üçüncü taraf script'leri erteleyin ya da hiç yüklemeyin

Her eklenen script — canlı sohbet widget'ı, takip pikseli, harici bir form aracı — ana iş parçacığını bir süre meşgul eder. next/script dört farklı yükleme stratejisi sunar:

import Script from 'next/script'

<Script src="https://widget.example.com/embed.js" strategy="lazyOnload" />
  • beforeInteractive: sayfa etkileşimli olmadan önce çalışması gereken kritik script'ler için. Nadiren gerekir.
  • afterInteractive (varsayılan): sayfa yüklendikten hemen sonra çalışır.
  • lazyOnload: tarayıcı boşta kaldığında çalışır — sohbet widget'ı gibi hemen gerekmeyen script'ler için doğru seçim.
  • worker: script'i ana iş parçacığından ayrı bir worker'a taşır (deneysel, her script için uygun değildir).

Ama en iyi optimizasyon genelde script'i hiç eklememek. Eklemeden önce sorun: bu script gerçekten her sayfada mı gerekli, yoksa tek bir sayfada mı? Kullanılmıyorsa kaldırın — kod satırı eklemekten daha hızlı bir performans kazanımı yoktur.

Bir script'i kaldıramıyorsanız — bir ödeme sağlayıcısının SDK'sı, bir harita bileşeni gibi zorunlu bir bağımlılıksa — en azından onu ihtiyaç anına kadar ertelemeyi düşünün. Bir harita widget'ı sayfa açılır açılmaz değil, ziyaretçi haritanın olduğu bölüme kaydırdığında ya da bir butona tıkladığında yüklenebilir. Next.js'te bunu dynamic() ile bir bileşeni istemci tarafında, gerektiğinde import ederek yapabilirsiniz; script sayfanın geri kalanının yüklenmesini hiç beklemez.

Layout shift'e karşı yer ayırma

CLS'nin üçüncü kaynağı, boyutu önceden bilinmeyen içeriğin sayfaya sonradan girmesi: bir reklam, bir bildirim çubuğu, geç yüklenen bir bileşen. Çözüm aynı prensibe dayanıyor — boyutu baştan ayırın:

.media-container {
  aspect-ratio: 16 / 9;
}

.skeleton-card {
  min-height: 240px;
}

aspect-ratio, içerik henüz gelmeden önce tarayıcıya doğru oranda bir kutu ayırtır; içerik geldiğinde etrafındaki hiçbir şey yer değiştirmez. Dinamik yüklenen bir bileşen için de aynı mantık geçerli: yükleme durumunda gösterdiğiniz iskelet (skeleton) elemanının yüksekliği, gerçek içeriğin ortalama yüksekliğine yakın olsun; rastgele küçük bir yer tutucu, içerik geldiğinde büyük bir sıçramaya dönüşür.

Ölçüm: tahmin etmeyin, @next/bundle-analyzer ile bakın

Yukarıdaki dört başlığı uyguladıktan sonra hangisinin gerçekten işe yaradığını görmenin yolu ölçmek. Chrome DevTools'taki Lighthouse sekmesi genel skoru verir; hangi JavaScript dosyasının sayfanızı şişirdiğini görmek için bundle analyzer daha doğrudan bir araç:

npm install --save-dev @next/bundle-analyzer
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer'

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
})

export default withBundleAnalyzer(nextConfig)
ANALYZE=true npm run build

Bu komut, her sayfanın hangi paketlerden oluştuğunu gösteren bir görsel harita açar; her dikdörtgen bir modüldür, boyutu o modülün nihai pakete kaç kilobayt kattığını gösterir. Genelde şaşırtıcı olan, kullanılmayan bir kütüphanenin ya da bir tarih kütüphanesinin tamamının tek bir tarih formatı için yüklenmiş olduğunu görmek. Bunu bulduktan sonra üç seçeneğiniz var: kütüphaneyi daha küçük bir alternatifle değiştirmek, yalnızca ihtiyacınız olan fonksiyonu import etmek (çoğu modern kütüphane bunu destekler), ya da o kütüphaneye yalnızca gerçekten ihtiyaç duyan sayfada dynamic() ile yüklemek. Her sayfanın aynı büyük paketi taşıması gerekmez; bir yönetim paneli sayfasının grafik kütüphanesi, o sayfayı hiç görmeyen bir ziyaretçinin ana sayfa yüklemesine karışmamalı.

Son not: Lighthouse skoru tek seferlik bir hedef değil, bir alışkanlık. Yeni bir bağımlılık eklediğinizde, yeni bir görsel yüklediğinizde skor değişir. Bu dört başlığı bir kere uygulayıp unutmak yerine, her önemli değişiklikten sonra tekrar ölçmek skoru 95'in üstünde tutmanın gerçek yolu.