Progressive Web Apps 2025: Offline-First, Install Edilebilir Web Uygulamaları
2025'te PWA'lar native app experience'ı web'e getiriyor. Service Workers ile offline-first, Web App Manifest ile installable, Push API ile engaging uygulamalar nasıl oluşturulur? iOS 17 ve modern browser desteğiyle PWA'ların golden age'i.
Progressive Web Apps 2025: Offline-First, Install Edilebilir Web Uygulamaları
Progressive Web Apps (PWA), 2025'te native mobile apps'e ciddi alternatif haline geldi. Apple'ın iOS 17 ile push notifications ve install prompts desteği eklemesi, game-changer oldu. Artık iPhone kullanıcıları da PWA'ları home screen'e ekleyip, offline kullanıp, push notification alabiliyor.
PWA'lar sadece "mobile-friendly website" değil, gerçek app experience sunuyor: Offline çalışma, background sync, push notifications, device hardware access. Ve tüm bunlar tek bir codebase ile, App Store approval beklemeden, instant updates ile.
Starbucks PWA'sı sadece 0.2MB (native app ~150MB), ancak tüm core functionality'yi offline sunuyor. Bu, PWA'ların gücünü gösteriyor.
Bu kapsamlı rehberde, modern PWA development'ın tüm aspectlerini - Service Workers, caching strategies, Web App Manifest, installation prompts, ve best practices'i detaylı inceleyeceğiz.
İçindekiler
- PWA Nedir ve Neden Önemli?
- Service Workers ile Offline-First
- Caching Strategies
- Web App Manifest
- Installation Experience
- Push Notifications
- Background Sync
- iOS Support 2025
- Performance Best Practices
- Sık Sorulan Sorular
PWA Nedir ve Neden Önemli?
Progressive Web App, modern web capabilities kullanarak native app experience sağlayan web uygulamasıdır.
PWA'nın Core Özellikleri
- Progressive: Her browser'da çalışır, progressively enhance eder
- Responsive: Her screen size'da düzgün görünür
- Offline-Capable: Service Workers ile network olmadan çalışır
- App-like: Native app gibi look & feel
- Fresh: Service Workers ile her zaman güncel
- Safe: HTTPS zorunlu
- Discoverable: Search engines tarafından index edilebilir
- Installable: Home screen'e eklenebilir
- Linkable: URL ile paylaşılabilir
Native App vs PWA
| Feature | Native App | PWA |
|---|---|---|
| Installation | App Store | Browser/Web |
| Updates | User approval | Automatic |
| Size | 50-200MB+ | 0.2-5MB |
| Offline | ✅ | ✅ |
| Push Notif | ✅ | ✅ (iOS 17+) |
| Device Access | Full | Limited |
| SEO | ❌ | ✅ |
| Cross-platform | Separate code | Single codebase |
| Distribution | Store approval | Instant |
Service Workers ile Offline-First
Service Worker, browser ile network arasında çalışan programmable proxy'dir. Offline functionality'nin temelidir.
Service Worker Registration
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js')
console.log('SW registered:', registration.scope)
// Update handling
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Yeni versiyon var, kullanıcıya bildir
showUpdateNotification()
}
})
})
} catch (error) {
console.error('SW registration failed:', error)
}
})
}
function showUpdateNotification() {
const notification = document.createElement('div')
notification.innerHTML = `
<p>Yeni versiyon mevcut!</p>
<button onclick="window.location.reload()">Güncelle</button>
`
document.body.appendChild(notification)
}Service Worker Lifecycle
// sw.js
const CACHE_VERSION = 'v1.0.0'
const CACHE_NAME = `app-cache-${CACHE_VERSION}`
const PRECACHE_URLS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html',
'/images/logo.png'
]
// Install - Cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing...')
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[SW] Precaching app shell')
return cache.addAll(PRECACHE_URLS)
}).then(() => {
// Yeni SW'yi hemen aktif et
return self.skipWaiting()
})
)
})
// Activate - Clean up old caches
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...')
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => {
console.log('[SW] Deleting old cache:', name)
return caches.delete(name)
})
)
}).then(() => {
// Tüm clients'ı hemen kontrol et
return self.clients.claim()
})
)
})
// Fetch - Serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse
}
return fetch(event.request).catch(() => {
// Network fail, offline page göster
if (event.request.mode === 'navigate') {
return caches.match('/offline.html')
}
})
})
)
})Caching Strategies
Farklı resource types için farklı caching stratejileri kullanılır.
1. Cache First (Offline First)
// Static assets için ideal
async function cacheFirst(request) {
const cachedResponse = await caches.match(request)
return cachedResponse || fetch(request)
}2. Network First
// API requests için ideal - fresh data öncelikli
async function networkFirst(request) {
try {
const response = await fetch(request)
const cache = await caches.open(CACHE_NAME)
cache.put(request, response.clone())
return response
} catch (error) {
return caches.match(request)
}
}3. Stale While Revalidate
// Hem hızlı hem fresh
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME)
const cachedResponse = await cache.match(request)
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone())
return response
})
return cachedResponse || fetchPromise
}4. Comprehensive Fetch Handler
// sw.js - Production-ready fetch handler
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)
// API requests - Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request))
return
}
// Images - Stale While Revalidate
if (request.destination === 'image') {
event.respondWith(staleWhileRevalidate(request))
return
}
// Static assets - Cache First
if (url.pathname.match(/\.(js|css|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(request))
return
}
// HTML pages - Network First with offline fallback
if (request.mode === 'navigate') {
event.respondWith(networkFirst(request))
return
}
// Default - Network only
event.respondWith(fetch(request))
})Web App Manifest
Web App Manifest, PWA'nın metadata'sını tanımlar ve installation experience'ı kontrol eder.
Manifest Dosyası
{
"name": "My Awesome PWA",
"short_name": "MyPWA",
"description": "A progressive web app that works offline",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4A90E2",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshots/home.png",
"sizes": "540x720",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/screenshots/dashboard.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
],
"categories": ["productivity", "utilities"],
"shortcuts": [
{
"name": "New Note",
"short_name": "Note",
"description": "Create a new note",
"url": "/new-note?source=shortcut",
"icons": [
{
"src": "/icons/new-note.png",
"sizes": "192x192"
}
]
}
]
}HTML'de Manifest Link
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Theme color -->
<meta name="theme-color" content="#4A90E2">
<!-- Apple-specific meta tags -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="MyPWA">
<link rel="apple-touch-icon" href="/icons/icon-180x180.png">
<!-- Windows -->
<meta name="msapplication-TileColor" content="#4A90E2">
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png">
<title>My PWA</title>
</head>
<body>
<!-- App content -->
<script src="/app.js"></script>
</body>
</html>Installation Experience
Installation Prompt Handling
// app.js
let deferredPrompt
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent automatic prompt
e.preventDefault()
// Store event for later use
deferredPrompt = e
// Show custom install button
showInstallButton()
})
function showInstallButton() {
const installButton = document.getElementById('install-button')
installButton.style.display = 'block'
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return
// Show install prompt
deferredPrompt.prompt()
// Wait for user choice
const { outcome } = await deferredPrompt.userChoice
console.log(`User response: ${outcome}`)
// Clear prompt
deferredPrompt = null
installButton.style.display = 'none'
// Track installation
if (outcome === 'accepted') {
analytics.track('PWA_INSTALLED')
}
})
}
// Detect if app is installed
window.addEventListener('appinstalled', () => {
console.log('PWA installed successfully')
deferredPrompt = null
analytics.track('PWA_INSTALL_SUCCESS')
})
// Check if already installed
if (window.matchMedia('(display-mode: standalone)').matches) {
console.log('Running as installed PWA')
hideBrowserUI()
}Install Criteria
PWA install edilebilmesi için:
- HTTPS ile serve edilmeli
- Web App Manifest olmalı
- Service Worker registered olmalı
- Minimum icon requirements (192x192, 512x512)
start_urltanımlı olmalıdisplay: standalone/fullscreen/minimal-uiolmalı
Push Notifications
Push Subscription
// Request notification permission
async function requestNotificationPermission() {
const permission = await Notification.requestPermission()
if (permission === 'granted') {
await subscribeUserToPush()
}
}
// Subscribe to push
async function subscribeUserToPush() {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
})
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
})
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = atob(base64)
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)))
}Handling Push Events
// sw.js
self.addEventListener('push', (event) => {
const data = event.data.json()
const options = {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
vibrate: [200, 100, 200],
data: {
url: data.url,
timestamp: Date.now()
},
actions: [
{
action: 'open',
title: 'Aç',
icon: '/icons/open.png'
},
{
action: 'dismiss',
title: 'Kapat',
icon: '/icons/close.png'
}
]
}
event.waitUntil(
self.registration.showNotification(data.title, options)
)
})
// Notification click handler
self.addEventListener('notificationclick', (event) => {
event.notification.close()
if (event.action === 'open') {
event.waitUntil(
clients.openWindow(event.notification.data.url)
)
}
})Background Sync
Background Sync, network geri geldiğinde pending işlemleri otomatik yürütür.
// Register background sync
async function saveDataWithSync(data) {
try {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(data)
})
} catch (error) {
// Network failure, register background sync
const registration = await navigator.serviceWorker.ready
await registration.sync.register('sync-data')
// Save data locally
await saveToIndexedDB(data)
}
}
// sw.js - Handle sync event
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-data') {
event.waitUntil(syncPendingData())
}
})
async function syncPendingData() {
const pendingData = await getFromIndexedDB()
for (const item of pendingData) {
try {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(item)
})
await removeFromIndexedDB(item.id)
} catch (error) {
console.error('Sync failed for item:', item.id)
// Will retry on next sync
}
}
}iOS Support 2025
iOS 17 ve macOS Sonoma ile Apple, PWA desteğini ciddi şekilde geliştirdi.
iOS'ta Yeni Desteklenen Özellikler
- ✅ Push Notifications
- ✅ Install Prompts
- ✅ Offline Functionality
- ✅ Web App Manifest
- ✅ Add to Home Screen banners
- ✅ Background Sync (partial)
iOS-Specific Considerations
<!-- iOS için özel meta tags -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="MyPWA">
<!-- iOS icons (180x180 zorunlu) -->
<link rel="apple-touch-icon" href="/icons/icon-180x180.png">
<link rel="apple-touch-startup-image" href="/splash-screen.png">// Detect iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
// iOS-specific handling
if (isIOS && !window.navigator.standalone) {
// Show "Add to Home Screen" instructions
showIOSInstallInstructions()
}Performance Best Practices
Critical Resources Precache
// sw.js - Smart precaching
const CRITICAL_ASSETS = [
'/',
'/app.js',
'/styles.css',
'/offline.html'
]
const OPTIONAL_ASSETS = [
'/icons/icon-192x192.png',
'/fonts/main.woff2'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(async (cache) => {
// Precache critical
await cache.addAll(CRITICAL_ASSETS)
// Precache optional (don't block install)
cache.addAll(OPTIONAL_ASSETS).catch(() => {
console.log('Optional assets failed to cache')
})
})
)
})Runtime Caching with Expiration
// sw.js - Cache with TTL
const MAX_AGE = 7 * 24 * 60 * 60 * 1000 // 7 days
async function cacheWithExpiration(request, response) {
const cache = await caches.open(CACHE_NAME)
const cachedResponse = response.clone()
const headers = new Headers(cachedResponse.headers)
headers.set('sw-cache-timestamp', Date.now().toString())
const modifiedResponse = new Response(cachedResponse.body, {
status: cachedResponse.status,
statusText: cachedResponse.statusText,
headers
})
await cache.put(request, modifiedResponse)
}
async function getCachedResponse(request) {
const cache = await caches.open(CACHE_NAME)
const cachedResponse = await cache.match(request)
if (!cachedResponse) return null
const timestamp = cachedResponse.headers.get('sw-cache-timestamp')
const age = Date.now() - parseInt(timestamp || '0')
if (age > MAX_AGE) {
await cache.delete(request)
return null
}
return cachedResponse
}Sık Sorulan Sorular
PWA SEO için nasıl?
PWA'lar search engines tarafından normal web pages gibi index edilir. Hatta offline capability ve fast load SEO için artı.
iOS'ta PWA native app kadar iyi mi?
iOS 17 ile çok yaklaştı. Push notifications, install prompts var. Ancak hala bazı kısıtlamalar var (örn: background tasks limited).
PWA App Store'da yayınlanabilir mi?
Evet, Microsoft Store ve Google Play PWA'ları kabul ediyor. App Store'da da Trusted Web Activity ile mümkün.
Service Worker güvenli mi?
Evet, HTTPS zorunlu. Service Worker man-in-the-middle attacks'ten korunmuş.
PWA'lar kaç MB cache kullanabilir?
Browser'a göre değişir. Chrome ~50GB, Safari ~1GB, Firefox ~50GB. Ancak quota management yapılmalı.
Offline database için ne kullanmalı?
IndexedDB modern ve güçlü. Daha kolay API için Dexie.js veya localForage kullanılabilir.
Sonuç
Progressive Web Apps, 2025'te web ve native app arasındaki çizgiyi bulanıklaştırdı. Özellikle iOS 17 desteğiyle, PWA'lar artık gerçek alternatif.
PWA'nın avantajları açık: Tek codebase, instant updates, SEO benefits, no app store approval. Dezavantajları ise azalıyor: Device access artıyor, performance gap kapanıyor.
E-ticaret, SaaS tools, content platforms için PWA perfect fit. Twitter Lite, Starbucks, Pinterest gibi büyük oyuncular zaten PWA kullanıyor.
Aksiyon Öğeleri:
- Mevcut web app'inize Web App Manifest ekleyin
- Service Worker ile offline support implement edin
- Installation prompt handle edin
- Push notifications test edin (iOS 17+ gerekli)
- Lighthouse ile PWA audit yapın
- Background sync ile offline form submissions implement edin
PWA'ların geleceği parlak. Early adopter olun, competitive advantage kazanın!
Projenizi Hayata Geçirelim
Web sitesi, mobil uygulama veya yapay zeka çözümü mü arıyorsunuz? Fikirlerinizi birlikte değerlendirelim.
Ücretsiz Danışmanlık Alın