Kembali ke Blog
Web Development#pwa#progressive web app#mobile#service worker#2026

Progressive Web App (PWA) di 2026: Install Website seperti Aplikasi Native

PWA memungkinkan website Anda diinstall di HP pengguna seperti aplikasi biasa, tanpa perlu Google Play atau App Store. Offline support, push notification, dan performa native — semua dari website.

Muhamad Putra Aulia Hidayat

Muhamad Putra Aulia Hidayat

11 Maret 20264 menit baca

Progressive Web App: Website yang Berasa Seperti Native App

Bayangkan website toko online Anda bisa di-install di HP pelanggan, muncul di home screen mereka, dan bisa diakses bahkan saat offline. Itu yang bisa dilakukan PWA.

Kenapa PWA Relevan di 2026?

  • Tidak perlu App Store — tidak ada review process, tidak ada 30% fee
  • Satu codebase — tidak perlu tim iOS dan Android terpisah
  • Update instan — tidak perlu user update manual
  • Biaya lebih rendah — develop dan maintain jauh lebih murah dari native app

Komponen Utama PWA

1. Web App Manifest

// public/manifest.json
{
  "name": "Toko Digital Uptime",
  "short_name": "DigiUpti",
  "description": "Platform layanan digital terpercaya",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#0a0a0a",
  "theme_color": "#2563eb",
  "orientation": "portrait",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/home.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    }
  ]
}

2. Service Worker

// public/sw.js
const CACHE_NAME = "digiupti-v1"
const STATIC_ASSETS = [
  "/",
  "/offline",
  "/icons/icon-192x192.png"
]

// Install: cache static assets
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
  )
  self.skipWaiting()
})

// Activate: hapus cache lama
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(
        keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
      )
    )
  )
  self.clients.claim()
})

// Fetch: Network first, fallback to cache
self.addEventListener("fetch", (event) => {
  if (event.request.method !== "GET") return
  
  event.respondWith(
    fetch(event.request)
      .then(response => {
        // Cache successful responses
        const cloned = response.clone()
        caches.open(CACHE_NAME).then(cache => {
          cache.put(event.request, cloned)
        })
        return response
      })
      .catch(() => {
        // Offline fallback
        return caches.match(event.request)
          .then(cached => cached || caches.match("/offline"))
      })
  )
})

// Push notifications
self.addEventListener("push", (event) => {
  const data = event.data?.json()
  
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: "/icons/icon-192x192.png",
      badge: "/icons/badge-72x72.png",
      data: { url: data.url },
      actions: [
        { action: "open", title: "Lihat" },
        { action: "dismiss", title: "Tutup" }
      ]
    })
  )
})

3. Register Service Worker di Next.js

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <link rel="manifest" href="/manifest.json" />
        <meta name="theme-color" content="#2563eb" />
        <meta name="apple-mobile-web-app-capable" content="yes" />
        <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
        <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
      </head>
      <body>
        {children}
        <ServiceWorkerRegistration />
      </body>
    </html>
  )
}

// components/ServiceWorkerRegistration.tsx
"use client"
import { useEffect } from "react"

export function ServiceWorkerRegistration() {
  useEffect(() => {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js")
        .then(reg => console.log("SW registered:", reg.scope))
        .catch(err => console.log("SW error:", err))
    }
  }, [])
  
  return null
}

Push Notifications

// lib/push-notifications.ts
export async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready
  
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY
  })
  
  // Kirim subscription ke server
  await fetch("/api/push/subscribe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(subscription)
  })
  
  return subscription
}

// app/api/push/send/route.ts
import webpush from "web-push"

webpush.setVapidDetails(
  "mailto:hello@digiupti.com",
  process.env.VAPID_PUBLIC_KEY!,
  process.env.VAPID_PRIVATE_KEY!
)

export async function POST(req: Request) {
  const { userId, title, body, url } = await req.json()
  
  const subscription = await getSubscription(userId)
  
  await webpush.sendNotification(
    subscription,
    JSON.stringify({ title, body, url })
  )
  
  return Response.json({ sent: true })
}

Install Prompt

"use client"
import { useState, useEffect } from "react"

export function InstallPrompt() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
  const [showBanner, setShowBanner] = useState(false)
  
  useEffect(() => {
    window.addEventListener("beforeinstallprompt", (e) => {
      e.preventDefault()
      setDeferredPrompt(e)
      setShowBanner(true)
    })
  }, [])
  
  const handleInstall = async () => {
    if (!deferredPrompt) return
    deferredPrompt.prompt()
    const { outcome } = await deferredPrompt.userChoice
    if (outcome === "accepted") setShowBanner(false)
  }
  
  if (!showBanner) return null
  
  return (
    <div className="fixed bottom-4 left-4 right-4 bg-primary text-primary-foreground p-4 rounded-xl shadow-lg">
      <p className="font-semibold">Install aplikasi kami</p>
      <p className="text-sm opacity-80">Akses lebih cepat langsung dari home screen</p>
      <div className="flex gap-2 mt-3">
        <button onClick={handleInstall} className="px-4 py-2 bg-white text-primary rounded-lg text-sm font-medium">
          Install
        </button>
        <button onClick={() => setShowBanner(false)} className="px-4 py-2 opacity-70 text-sm">
          Nanti saja
        </button>
      </div>
    </div>
  )
}

PWA adalah sweet spot antara web dan native app — terutama untuk pasar Indonesia di mana pengguna sensitif terhadap storage HP dan kuota internet.

pwaprogressive web appmobileservice worker2026

Newsletter Digital Uptime

Tips teknologi & bisnis mingguan

Bergabung dengan 2,500+ subscriber yang mendapatkan insight teknologi, tutorial development, dan tips bisnis digital langsung ke inbox mereka setiap minggu.

Tidak ada spam. Unsubscribe kapan saja.

Artikel Terkait

Kami menggunakan cookies untuk meningkatkan pengalaman Anda di website ini. Dengan melanjutkan, Anda menyetujui penggunaan cookies sesuai Kebijakan Privasi kami.