
Aşamalı Web Uygulaması (PWA), uygulamaya benzer bir deneyim sunmak için modern tarayıcı özelliklerini kullanan bir web uygulamasıdır. PWA'lar güvenilir, hızlı ve ilgi çekicidir; web erişimini yerel mobil uygulamaların yetenekleriyle birleştirir.
Aşamalı Web Uygulaması (PWA), uygulamaya benzer bir deneyim sunmak için modern tarayıcı özelliklerini kullanan bir web uygulamasıdır. PWA'lar güvenilir, hızlı ve ilgi çekicidir; web erişimini yerel mobil uygulamaların yetenekleriyle birleştirir.
PWA'lar temel bir sorunu çözüyor: Kullanıcılar web'in anında kullanılabilirliğini istiyor (kurulum yok, bağlantı kurulabilir, her zaman güncel) ancak yerel uygulamaların performansını ve özelliklerini (çevrimdışı destek, anında bildirimler, ana ekran varlığı) bekliyorlar. PWA'lar her ikisini de sağlar.
"Progresif Web Uygulaması" terimi, 2015 yılında Google'dan Alex Russell ve Frances Berriman tarafından icat edildi. Bir PWA'nın şu 10 özelliği karşılaması gerekir:
| # | Prensip | Neden Önemlidir? |
|---|---|---|
| 1 | Aşamalı — Herkes için işe yarar | Eski tarayıcılarda zarif bozulma |
| 2 | Duyarlı — Her ekrana sığar | Masaüstü, tablet, mobil |
| 3 | Bağlantıdan bağımsız — Çevrimdışı çalışır | Hizmet çalışanının önbelleğe alınması |
| 4 | Uygulama benzeri — Yerel uygulama hissi | Kabuk mimarisi, sorunsuz gezinme |
| 5 | Taze — Her zaman güncel | Hizmet çalışanı güncelleme yaşam döngüsü |
| 6 | Güvenli — HTTPS üzerinden sunulur | Servis çalışanları için gerekli olan kurcalamayı önler |
| 7 | Keşfedilebilir — SEO dostu | Arama motorları PWA'ları dizine ekler |
| 8 | Yeniden etkinleştirilebilir — Anlık bildirimler | Yerel uygulamalar gibi kullanıcılarla yeniden etkileşime geçin |
| 9 | Yüklenebilir — Ana ekrana ekle | Web uygulaması bildirimi |
| 10 | Bağlanabilir — URL aracılığıyla paylaşın | Uygulama mağazasına gerek yok |
Hizmet çalışanı, web sayfasından ayrı olarak arka planda çalışan bir JavaScript dosyasıdır. Programlanabilir bir ağ proxy'si görevi görerek istekleri yakalar ve çevrimdışı işlevsellik, anında bildirimler ve arka planda senkronizasyon sağlar.
Installing → Installed → Activating → Activated → Idle → Terminated
│ │
└─── (new version detected) ──────────────┘
└─── Waiting (until all tabs close) ─────┘
// service-worker.js
const CACHE_NAME = 'my-pwa-v2';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html',
'/icons/icon-192.png'
];
// Install event — cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Caching app shell');
return cache.addAll(ASSETS_TO_CACHE);
})
);
// Force new service worker to activate immediately
self.skipWaiting();
});
// Activate event — clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
// Take control of all open pages immediately
clients.claim();
});
// Fetch event — intercept network requests
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
// Return cached response if available
if (cachedResponse) {
return cachedResponse;
}
// Otherwise fetch from network
return fetch(event.request).then(networkResponse => {
// Don't cache non-GET or error responses
if (!event.request.url.includes('/api/')) {
return networkResponse;
}
// Cache API responses for offline use
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
return networkResponse;
}).catch(() => {
// Network failed — return offline fallback
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
return new Response('Offline', { status: 503 });
});
})
);
});
| Strateji | Açıklama | Kullanım Örneği |
|---|---|---|
| Önce Önbellek | Önbelleği kontrol edin, ağa geri dönün | Statik varlıklar (CSS, JS, resimler) |
| Önce Ağ | Ağı deneyin, önbelleğe geri dönün | API çağrıları, dinamik içerik |
| **Yeniden Doğrulama Sırasında Eski ** | Önbelleği döndür, arka planda güncelle | Haber akışları, blog gönderileri |
| Yalnızca Ağ | Her zaman ağdan getir | Hassas veriler (bankacılık) |
| Yalnızca Önbellek | Hiçbir zaman ağdan getirme | Uygulama kabuğu |
// Stale-while-revalidate strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
}).catch(() => cachedResponse);
return cachedResponse || fetchPromise;
})
);
});
Bildiri, PWA'nın yüklendiğinde nasıl görüneceğini kontrol eden bir JSON dosyasıdır:
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "An amazing PWA that works offline",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3367D6",
"orientation": "portrait-primary",
"scope": "/",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["productivity", "utilities"],
"lang": "en-US",
"dir": "ltr",
"prefer_related_applications": false
}
Açıklanan manifest özellikleri:
display: standalone — Tarayıcı kullanıcı arayüzü (adres çubuğu, sekmeler) olmadan açılır.display: fullscreen — Tam ekran, tarayıcı kromu yok.display: minimal-ui — Minimum tarayıcı kontrolü.scope: "/" — Hangi URL'lerin PWA'nın parçası olduğu.start_url — PWA başlatıldığında açılan sayfa.Hizmet çalışanları güvenli bir bağlama ihtiyaç duyar. HTTPS aşağıdakiler için zorunludur:
# Redirect HTTP to HTTPS
# Nginx
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
Uygulama kabuğu, kullanıcı arayüzünü oluşturmak için gereken minimum HTML, CSS ve JavaScript'tir. İlk yüklemede önbelleğe alınır ve PWA'nın temeli olarak hizmet eder.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367D6">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/icons/icon-192.png" sizes="192x192">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<title>My PWA</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header id="app-header">My PWA</header>
<main id="app-content"></main>
<footer id="app-footer">© 2026</footer>
<!-- Register service worker -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW registration failed:', err));
});
}
</script>
<script src="/app.js"></script>
</body>
</html>
// app.js — Fetch content with offline support
const CACHE_KEY = 'content-cache';
async function loadContent(url) {
try {
const response = await fetch(url);
const data = await response.json();
// Cache for offline
const cache = await caches.open(CACHE_KEY);
cache.put(url, new Response(JSON.stringify(data)));
renderContent(data);
} catch (error) {
// Offline — load from cache
const cached = await caches.match(url);
if (cached) {
const data = await cached.json();
renderContent(data);
showOfflineIndicator();
} else {
showOfflineMessage();
}
}
}
// Request permission
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
// Get push subscription
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription)
});
}
}
// service-worker.js — Handle push events
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
vibrate: [100, 50, 100],
data: { url: data.url },
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Dismiss' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'open' || !event.action) {
clients.openWindow(event.notification.data.url);
}
});
| Özellik | PWA | Yerel Uygulama |
|---|---|---|
| Dağıtım | URL (mağaza gerekmez) | App Store, Google Play |
| Sürtünmeyi kurun | Tek dokunuşla (tarayıcı istemi) | İndirme, yükleme, izinler |
| Dosya boyutu | KB'den MB'ye | 10MB - 500MB+ |
| Güncellemeler | Anında (hizmet çalışanı) | Uygulama mağazası incelemesi yoluyla |
| Çevrimdışı | Evet (hizmet çalışanı) | Evet (yerleşik) |
| Anlık bildirimler | Evet | Evet |
| Cihaz API'leri | Kamera, GPS, ivmeölçer, ödeme | Tüm cihaz API'lerine tam erişim |
| Bluetooth | Web Bluetooth (sınırlı) | Tam Bluetooth API'si |
| Arka plan görevleri | Arka planda senkronizasyon (sınırlı) | Tam arka planda yürütme |
| Performans | Yerele yakın (JavaScript/WebAssembly) | Yerel (derlenmiş kod) |
| SEO | Arama motorları tarafından indekslenebilir | Dizine eklenemez |
| Geliştirme maliyeti | Tek kod tabanı | iOS + Android (veya platformlar arası) |
| Kullanıcıyı elde tutma | Daha düşük (ayrılması daha kolay) | Daha yüksek (kurulu = kararlı) |
| Metrik | PWA'dan önce | PWA'dan sonra | İyileştirme |
|---|---|---|---|
| İlk yükleme | 5'ler | 1,2 saniye | %76 daha hızlı |
| Sonraki yükler | 3'ler | 0,3s | %90 daha hızlı |
| Hemen çıkma oranı | 45% | 20% | %55 azalma |
| Dönüşüm oranı | 2% | 3.5% | %75 artış |
| Oturum başına sayfa sayısı | 3 | 5 | %66 artış |
Gerçek dünya sonuçları:
| Şirket | Anahtar Metrik | İyileştirme |
|---|---|---|
| Twitter (X) Lite | Oturum başına gönderilen Tweetler | %75 artış |
| Pinterest'te | Harcanan zaman | %40 artış |
| Über | Rezervasyonun tamamlanması | 2G'de bile sorunsuz |
| Starbucks | Günlük aktif kullanıcılar | 2 kat artış |
| Alibaba'nın | Dönüşüm oranı | %76 artış |
# Run Lighthouse from CLI
npx lighthouse https://example.com --view --preset=desktop
# PWA-specific checks:
# - ✅ Registers a service worker
# - ✅ Responds with 200 when offline
# - ✅ Web app manifest exists
# - ✅ Manifest has display: standalone
# - ✅ Served over HTTPS
# - ✅ Redirects HTTP to HTTPS
# - ✅ Configurable start URL
# - ✅ Icons provided (192px and 512px)
## PWA Pre-Flight Checklist
- [ ] HTTPS enabled
- [ ] Service worker registered and active
- [ ] Pages load offline (test with airplane mode)
- [ ] Manifest configured with correct icons
- [ ] "Add to Home Screen" prompt appears
- [ ] PWA opens in standalone mode with no address bar
- [ ] Splash screen displays correctly
- [ ] Push notifications work
- [ ] All pages are responsive (mobile, tablet, desktop)
- [ ] Performance budget met (Lighthouse score > 90)
- [ ] Cross-browser tested (Chrome, Firefox, Safari, Edge)
- [ ] Accessibility audit passes
// Register a sync event
async function scheduleSync() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-pending-data');
}
// Service worker handles sync
self.addEventListener('sync', event => {
if (event.tag === 'sync-pending-data') {
event.waitUntil(syncPendingData());
}
});
// Request periodic sync (requires user engagement)
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register('update-content', {
minInterval: 24 * 60 * 60 * 1000 // Once per day
});
// Share content using native share sheet
async function shareContent(title, text, url) {
if (navigator.share) {
await navigator.share({ title, text, url });
} else {
// Fallback to custom share UI
copyToClipboard(url);
}
}
// Set app icon badge
if (navigator.setAppBadge) {
await navigator.setAppBadge(5); // Show "5" on icon
} else if (navigator.clearAppBadge) {
await navigator.clearAppBadge();
}
| Tuzak | Sorun | Çözüm |
|---|---|---|
| Aşırı önbelleğe alma | Uygulama eski içerik sunuyor | Sürüm önbellekleri, kabuk için önbelleği öncelikli, içerik için öncelikli ağı uygulayın |
| Çevrimdışı geri dönüş yok | Beyaz ekran çevrimdışı | /offline.html'yi her zaman anlamlı bir mesajla önbelleğe alın |
| Önbellek bozma hataları | Eski servis çalışanı ısrar ediyor | skipWaiting() ve client.claim()'i kullanın |
| Açılış ekranı sorunları | Yanlış tema rengi veya simgesi | Bildiriyi doğru şekilde yapılandırın, maskelenebilir simgeler kullanın |
| Safari uyumluluğu | Bazı özellikler sınırlıdır (itme, periyodik senkronizasyon) | Aşamalı iyileştirme — zarif bir şekilde bozulma |
| Büyük önbellek boyutu | Depolama kotası aşıldı (kaynak başına 50 MB) | Eski önbellekleri budayın, önbellek boyutunu sınırlayın |
| Başlangıç_url'si eksik | Ana ekran yanlış sayfayı açıyor | Bildirimde her zaman start_url'yi belirtin |
Progresif Web Uygulamaları her iki dünyanın da en iyi yönlerini temsil eder; web'in erişimi ve erişilebilirliği ile yerel uygulamaların yetenekleri ve kullanıcı deneyimi bir araya gelir.
PWA'lar her senaryoda yerel uygulamaların yerini almaz; ancak içerik odaklı ve yardımcı program uygulamalarının çoğu için, geliştirme maliyetinin çok küçük bir kısmıyla üstün bir kullanıcı deneyimi sağlarlar.
Henüz onaylı yorum yok. Yeni yanıtlar moderasyon bekleyebilir.