Cloudflare Pages'e deploy ettiğimiz bir Nuxt 3 e-ticaret projesi PageSpeed Insights'ta LCP 10.2s gösteriyordu. Google Fonts, client-side hydration, above-the-fold yüklemesi ve CDN cache header'ları klasik darboğazlardı. Self-hosted font subsetting, Vue 3 lazy hydration API'si, CSS content-visibility ve Cloudflare edge cache TTL ayarlarıyla LCP'yi 2.1s'ye düşürdük. Bu yazıda dört müdahalenin teknik detayı ve benchmark sonuçları var.

Self-Hosted Font Subsetting ile FCP 900ms Düşüş

Google Fonts CSS dosyası 320ms render-blocking request'ti. Variable font WOFF2 indirmesi sonrası First Contentful Paint 3.8s civarına geliyordu. @fontsource paketini yükleyip sadece Latin subset + 400-700 weight range'i seçtik:

npm install @fontsource-variable/inter

app.vue import:

import '@fontsource-variable/inter/wght.css';

nuxt.config.ts içinde font-display ayarı:

export default defineNuxtConfig({
  css: ['@fontsource-variable/inter/wght.css'],
  vite: {
    css: {
      postcss: {
        plugins: [
          require('postcss-preset-env')({
            features: { 'custom-properties': false }
          })
        ]
      }
    }
  }
});

Sonuç: WOFF2 dosyası 24KB, ilk request'te inline olarak servis edildi. FCP 3.8s → 2.9s. Render-blocking süresi 320ms → 0ms. Variable font axes özelliğini korumak için wght.css import ettik, statik weight dosyaları yerine.

Google Fonts'un CDN edge location sayısı fazla ama DNS lookup + TLS handshake her ziyaretçi için 200-300ms ekliyordu. Self-hosted setup ile origin server'dan servis hızı Cloudflare Pages edge'inde aynı, ancak ek DNS hop'u elimine ettik.

Lazy Hydration ile TBT 2190ms → 200ms

Nuxt 3 varsayılan olarak tüm component'leri client-side hydrate eder. Product listing page'de 48 ürün kartı vardı, her biri Vue reactivity sistemi için 120KB JavaScript parse'ladı. Total Blocking Time 2190ms — kullanıcı 2 saniye boyunca sayfa scroll edemiyor.

Vue 3.5+ defineAsyncComponent + hydration:lazy kullanarak below-the-fold component'leri lazy hydrate ettik:

// components/ProductCard.vue
<script setup>
defineOptions({
  hydration: 'lazy'
});
</script>

Intersection Observer ile viewport'a girenler hydrate olsun:

// plugins/lazy-hydration.client.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.mixin({
    mounted() {
      if (this.$options.hydration === 'lazy') {
        const observer = new IntersectionObserver((entries) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              this.$forceUpdate();
              observer.disconnect();
            }
          });
        });
        observer.observe(this.$el);
      }
    }
  });
});

Above-the-fold hero + ilk 6 ürün immediate hydrate, geri kalanı lazy. Bundle size 480KB → 280KB initial, 200KB lazy chunk. TBT 2190ms → 200ms. Kullanıcı 1 saniyede scroll edebiliyor.

Tradeoff: Lazy hydration event listener attach gecikmesi. Click handler'ı olan component için hydration: 'immediate' kaldık (Add to Cart button). Scroll-triggered content için lazy ideal.

Nuxt'ın Built-in Lazy Component'i

Nuxt 3.0+ ile gelen <LazyComponentName> prefix'i de aynı işi yapar:

<template>
  <LazyProductCard v-for="product in products" :key="product.id" />
</template>

Ancak bu yöntem component'i server-side render etmez, client-side mount eder. Bizim setup'ta SEO için SSR gerekiyordu, bu yüzden defineOptions yöntemi tercih ettik.

CSS content-visibility ile LCP 1.4s Kazanç

Product grid'de 48 kart rendering layout shift'e sebep oluyordu. Browser her kartı render edip CLS hesaplıyor, LCP gecikmesi artıyor. content-visibility: auto kullanarak off-screen content'i render cycle'dan çıkardık:

.product-card {
  content-visibility: auto;
  contain-intrinsic-size: 0 360px;
}

contain-intrinsic-size browser'a "bu element 360px yüksekliğinde" diye hint veriyor, viewport dışındayken placeholder yüksekliği tutuyor. Layout shift CLS 0.18 → 0.02.

Benchmark (Lighthouse 10.4, throttled 4G):

MetrikÖnceSonraDelta
LCP10.2s2.1s-8.1s
CLS0.180.02-0.16
TBT2190ms200ms-1990ms

content-visibility Safari 17+ support var, iOS 16'da fallback olarak normal render. @supports kullanarak progressive enhancement:

@supports (content-visibility: auto) {
  .product-card {
    content-visibility: auto;
    contain-intrinsic-size: 0 360px;
  }
}

Bu yaklaşım UI/UX Tasarım sürecinde layout stability için kritik. Kullanıcı tecrübesi viewport dışı content'in render maliyetinden bağımsız hale geldi.

Cloudflare Pages Edge Cache TTL Optimizasyonu

Cloudflare Pages default edge cache TTL'si 2 saat. Product pricing 15 dakikada bir güncelleniyor, ancak görsel asset'ler (resim, font) 7 gün static. _headers dosyası ile granular cache control:

# _headers
/assets/*
  Cache-Control: public, max-age=604800, immutable

/_nuxt/*
  Cache-Control: public, max-age=31536000, immutable

/api/*
  Cache-Control: public, s-maxage=900, stale-while-revalidate=60

/*
  Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=300
  • /assets/* ve /_nuxt/*: 1 yıl immutable (fingerprint hash var, URL değişince yeni dosya)
  • /api/*: 15 dakika edge cache, 60 saniye stale-while-revalidate (origin çöktüğünde eski data servis et)
  • Root HTML: 1 saat edge cache, 5 dakika stale-while-revalidate

Time to First Byte edge location'dan 40ms, origin'den 280ms oluyordu. Edge hit rate %89 → %96. TTFB median 280ms → 45ms.

stale-while-revalidate kullanıcı için kritik: Origin güncelleme yapıyorsa eski cache'i servis et, background'da yeni veriyi çek. Kullanıcı hiç beklemez.

Cloudflare KV ile Dynamic Cache Purge

Pricing güncellemesi olduğunda tüm cache'i purge etmek yerine Cloudflare KV + Workers ile selective invalidation:

// workers/cache-purge.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const productId = url.searchParams.get('id');
    
    const cacheKey = `product:${productId}`;
    await env.CACHE_KV.delete(cacheKey);
    
    return new Response('Cache purged', { status: 200 });
  }
};

Admin panel pricing güncellemesi → webhook → Cloudflare Worker → KV delete. Edge cache TTL korunur, sadece değişen product'lar invalidate olur.

Performans İzleme ve Regression Prevention

RUM (Real User Monitoring) için Cloudflare Web Analytics + custom Navigation Timing beacon:

// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
  if (typeof window !== 'undefined') {
    window.addEventListener('load', () => {
      const perfData = performance.getEntriesByType('navigation')[0];
      const lcp = performance.getEntriesByType('largest-contentful-paint')[0];
      
      fetch('/api/perf', {
        method: 'POST',
        body: JSON.stringify({
          ttfb: perfData.responseStart - perfData.requestStart,
          fcp: perfData.domContentLoadedEventEnd - perfData.fetchStart,
          lcp: lcp?.renderTime || 0,
          pathname: window.location.pathname
        })
      });
    });
  }
});

BigQuery'de günlük P75 LCP takibi. 2.5s threshold geçerse Slack alert. CI/CD pipeline'da Lighthouse CI ile regression check:

# .github/workflows/lighthouse.yml
- name: Lighthouse CI
  run: |
    npm install -g @lhci/cli
    lhci autorun --config=./lighthouserc.json

lighthouserc.json içinde LCP assertion:

{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }]
      }
    }
  }
}

Deploy öncesi LCP 2.5s üstüyse build fail. Production'da regression önleniyor.

Tradeoff ve Edge Case'ler

Lazy hydration scroll position'a bağlı. Kullanıcı hızlı scroll ederse hydration gecikmesi interactivity'yi etkiler. Mitigation: Intersection Observer'ın rootMargin: '100px' ile viewport'a girmeden 100px önce trigger et.

content-visibility grid layout'larda column count değişikliğinde CLS artışı yapabilir. Sabit grid-template-columns + contain-intrinsic-size kombinasyonu zorunlu.

Edge cache stale-while-revalidate pricing tutarsızlığı riski var: Kullanıcı A eski fiyat, kullanıcı B yeni fiyat görebilir. Business requirement'a göre karar: E-ticaret için 60 saniye stale window kabul edilebilir, fintech için edilemez.

Self-hosted font license kontrolü gerekir. Google Fonts SIL Open Font License ile serbest, ticari font için licensing agreement check edin.

Bu dört müdahale LCP'yi 80% iyileştirdi. Nuxt 3'ün Vue 3 reactivity system'i lazy hydration için ideal. Cloudflare Pages edge network'ü CDN olarak yeterli, ancak dynamic content için KV + Workers kombinasyonu cache granularity sağlıyor. Production'da RUM + Lighthouse CI regression prevention zorunlu.