Progressive Web Apps 2026: Offline-First với Service Worker, Workbox và Vue.js

Posted on: 4/26/2026 12:29:09 PM

Người dùng kỳ vọng ứng dụng web hoạt động mượt như app native — tải nhanh, chạy offline, gửi push notification và cài đặt trực tiếp từ trình duyệt mà không qua App Store. Progressive Web Apps (PWA) biến kỳ vọng đó thành hiện thực, và năm 2026, mọi trình duyệt lớn đều hỗ trợ đầy đủ các API cốt lõi: Service Worker, Web App Manifest và Web Push — kể cả Safari trên iOS.

2-3xTốc độ tải lại nhanh hơn nhờ Service Worker cache
68%Tăng mobile engagement khi dùng PWA
1 codebaseChạy trên mọi nền tảng: Desktop, Mobile, Tablet
$0Phí App Store — cài trực tiếp từ browser

1. PWA là gì và tại sao quan trọng?

PWA là ứng dụng web được tăng cường bằng ba công nghệ cốt lõi: Service Worker (proxy mạng chạy nền, cho phép offline và caching), Web App Manifest (file JSON mô tả metadata để trình duyệt cho phép "cài đặt"), và HTTPS (bắt buộc vì Service Worker can thiệp vào network request). Kết hợp lại, chúng tạo ra trải nghiệm app-like mà không cần native code.

flowchart TB
    A["Người dùng truy cập Web App"] --> B{"Service Worker
đã cài?"} B -->|"Chưa"| C["Tải trang bình thường
+ Đăng ký SW"] B -->|"Rồi"| D["SW intercept request"] D --> E{"Có trong Cache?"} E -->|"Có"| F["Trả từ Cache
(instant load)"] E -->|"Không"| G["Fetch từ Network"] G --> H["Cache response mới"] C --> I{"Đủ điều kiện
installable?"} I -->|"Có"| J["Hiển thị Install Prompt"] J --> K["Cài PWA lên Home Screen"] style D fill:#e94560,stroke:#fff,color:#fff style F fill:#4CAF50,stroke:#fff,color:#fff style K fill:#2c3e50,stroke:#fff,color:#fff
Vòng đời PWA: từ lần truy cập đầu tiên đến cài đặt trên Home Screen

2. Thiết lập PWA với Vue 3 và Vite

2.1. Cài đặt vite-plugin-pwa

npm install -D vite-plugin-pwa

2.2. Cấu hình trong vite.config.ts

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { VitePWA } from 'vite-plugin-pwa' export default defineConfig({ plugins: [ vue(), VitePWA({ registerType: 'autoUpdate', includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'], manifest: { name: 'My Vue PWA', short_name: 'VuePWA', description: 'Vue 3 Progressive Web App', theme_color: '#e94560', background_color: '#ffffff', display: 'standalone', start_url: '/', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png' }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png' }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' } ] }, workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], runtimeCaching: [ { urlPattern: /^https:\/\/api\.example\.com\/.*/i, handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 50, maxAgeSeconds: 300 } } }, { urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/, handler: 'CacheFirst', options: { cacheName: 'image-cache', expiration: { maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 } } } ] } }) ] })

registerType: 'autoUpdate' vs 'prompt'

autoUpdate tự động cập nhật Service Worker khi có phiên bản mới — user không cần làm gì. prompt hiển thị thông báo cho user chọn cập nhật hay không — phù hợp khi bạn muốn kiểm soát UX chặt hơn (ví dụ app tài chính, nơi reload đột ngột có thể gây mất form data).

3. Service Worker — Bộ não của PWA

Service Worker là JavaScript chạy ở background thread, độc lập với trang web. Nó đóng vai trò proxy mạng — mọi HTTP request từ ứng dụng đều đi qua Service Worker trước khi ra internet, cho phép bạn quyết định: trả từ cache, fetch từ network, hay kết hợp cả hai.

3.1. Vòng đời Service Worker

flowchart LR
    A["Register"] --> B["Install"]
    B --> C["Activate"]
    C --> D["Running
(intercept fetch)"] D --> E{"Có SW mới?"} E -->|"Có"| F["Install mới
(waiting)"] F --> G["Activate mới
(thay thế cũ)"] G --> D E -->|"Không"| D style B fill:#e94560,stroke:#fff,color:#fff style C fill:#4CAF50,stroke:#fff,color:#fff style D fill:#2c3e50,stroke:#fff,color:#fff
Vòng đời Service Worker: install → activate → running → update

3.2. Custom Service Worker trong Vue

// src/sw.ts — custom service worker import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching' import { registerRoute } from 'workbox-routing' import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies' import { ExpirationPlugin } from 'workbox-expiration' declare let self: ServiceWorkerGlobalScope // Precache tất cả assets từ build precacheAndRoute(self.__WB_MANIFEST) cleanupOutdatedCaches() // API calls: Network First (ưu tiên data mới nhất) registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst({ cacheName: 'api-responses', plugins: [ new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }) ] }) ) // Font files: Cache First (ít thay đổi) registerRoute( ({ request }) => request.destination === 'font', new CacheFirst({ cacheName: 'font-cache', plugins: [ new ExpirationPlugin({ maxEntries: 20, maxAgeSeconds: 365 * 24 * 60 * 60 }) ] }) ) // Trang HTML: Stale While Revalidate (hiện cache cũ, cập nhật nền) registerRoute( ({ request }) => request.mode === 'navigate', new StaleWhileRevalidate({ cacheName: 'pages-cache' }) )

4. Chiến lược Caching

Chọn đúng caching strategy là yếu tố quyết định trải nghiệm PWA. Mỗi strategy phù hợp với một loại resource khác nhau:

StrategyCách hoạt độngDùng choTrade-off
Cache FirstKiểm tra cache trước, fetch network nếu cache missẢnh, font, CSS/JS đã hashNhanh nhất, nhưng data có thể cũ
Network FirstFetch network trước, fallback cache nếu offlineAPI response, data độngData luôn mới, nhưng chậm hơn
Stale While RevalidateTrả cache ngay, đồng thời fetch network cập nhật cacheTrang HTML, danh mục sản phẩmCân bằng tốc độ và freshness
Network OnlyLuôn fetch network, không dùng cacheAnalytics, payment APIKhông hoạt động offline
Cache OnlyLuôn trả từ cache, không fetch networkPrecached assetsNhanh nhất, không bao giờ cập nhật
flowchart LR
    subgraph CF["Cache First"]
        A1["Request"] --> A2{"Cache?"}
        A2 -->|"Hit"| A3["Return Cache"]
        A2 -->|"Miss"| A4["Fetch Network"]
    end

    subgraph NF["Network First"]
        B1["Request"] --> B2["Fetch Network"]
        B2 -->|"OK"| B3["Return + Cache"]
        B2 -->|"Fail"| B4["Return Cache"]
    end

    subgraph SWR["Stale While Revalidate"]
        C1["Request"] --> C2["Return Cache"]
        C1 --> C3["Fetch Network"]
        C3 --> C4["Update Cache"]
    end

    style A3 fill:#4CAF50,stroke:#fff,color:#fff
    style B3 fill:#4CAF50,stroke:#fff,color:#fff
    style C2 fill:#4CAF50,stroke:#fff,color:#fff
Ba chiến lược caching phổ biến nhất trong PWA

5. Web App Manifest — Biến web thành "app"

Web App Manifest là file JSON cho trình duyệt biết ứng dụng nên hiển thị thế nào khi được "cài đặt" — tên, icon, màu theme, display mode, và orientation. Đây là yêu cầu bắt buộc để trình duyệt hiện install prompt:

{ "name": "Vue E-Commerce PWA", "short_name": "VueShop", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#e94560", "description": "Mua sắm mọi lúc, kể cả khi offline", "categories": ["shopping", "lifestyle"], "orientation": "any", "icons": [ { "src": "/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png" }, { "src": "/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png" }, { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png" }, { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" }, { "src": "/icons/maskable-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ], "screenshots": [ { "src": "/screenshots/home.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" }, { "src": "/screenshots/home-mobile.png", "sizes": "750x1334", "type": "image/png", "form_factor": "narrow" } ] }

Điều kiện để trình duyệt hiện Install Prompt

Chrome yêu cầu: HTTPS, Service Worker đã register, Web App Manifest có name, icons (ít nhất 192px và 512px), start_url, display (standalone/fullscreen). Safari trên iOS hỗ trợ Add to Home Screen nhưng không hiện prompt tự động — user phải dùng Share → Add to Home Screen.

6. Custom Install Prompt với Vue 3

<script setup lang="ts"> import { ref, onMounted } from 'vue' const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null) const showInstallBanner = ref(false) onMounted(() => { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault() deferredPrompt.value = e as BeforeInstallPromptEvent showInstallBanner.value = true }) window.addEventListener('appinstalled', () => { showInstallBanner.value = false deferredPrompt.value = null }) }) async function installApp() { if (!deferredPrompt.value) return deferredPrompt.value.prompt() const { outcome } = await deferredPrompt.value.userChoice if (outcome === 'accepted') { showInstallBanner.value = false } deferredPrompt.value = null } </script> <template> <Transition name="slide-up"> <div v-if="showInstallBanner" class="install-banner"> <p>Cài ứng dụng để trải nghiệm nhanh hơn, offline</p> <button @click="installApp">Cài đặt</button> <button @click="showInstallBanner = false">Để sau</button> </div> </Transition> </template>

7. Push Notification

Push Notification là kênh duy nhất có thể tiếp cận user ngay cả khi tab đã đóng. Nó hoạt động thông qua Push API kết hợp Service Worker:

sequenceDiagram
    participant U as User (Browser)
    participant SW as Service Worker
    participant S as Your Server
    participant P as Push Service
(FCM / APNs) U->>SW: Notification.requestPermission() SW->>P: PushManager.subscribe() P-->>SW: PushSubscription (endpoint + keys) SW->>S: POST /api/push/subscribe S->>S: Lưu subscription Note over S: Khi có event mới... S->>P: POST endpoint (payload + VAPID) P->>SW: Push event SW->>U: self.registration.showNotification()
Luồng Push Notification: subscribe → server lưu → push qua FCM/APNs → SW hiển thị

7.1. Đăng ký Push Subscription

// composables/usePushNotification.ts export function usePushNotification() { async function subscribe() { const permission = await Notification.requestPermission() if (permission !== 'granted') return null const registration = await navigator.serviceWorker.ready const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array( import.meta.env.VITE_VAPID_PUBLIC_KEY ) }) // Gửi subscription lên server await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }) return subscription } return { subscribe } }

7.2. Nhận Push trong Service Worker

// Trong service worker self.addEventListener('push', (event) => { const data = event.data?.json() ?? { title: 'Thông báo mới', body: 'Bạn có cập nhật mới' } 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: 'Xem ngay' }, { action: 'dismiss', title: 'Bỏ qua' } ] }) ) }) self.addEventListener('notificationclick', (event) => { event.notification.close() if (event.action === 'open' || !event.action) { event.waitUntil( clients.openWindow(event.notification.data.url) ) } })

7.3. Gửi Push từ .NET Backend

// ASP.NET Core — gửi push notification using WebPush; public class PushNotificationService { private readonly VapidDetails _vapid; public PushNotificationService(IConfiguration config) { _vapid = new VapidDetails( "mailto:admin@example.com", config["Vapid:PublicKey"]!, config["Vapid:PrivateKey"]! ); } public async Task SendAsync(PushSubscription subscription, string title, string body, string url) { var client = new WebPushClient(); var payload = JsonSerializer.Serialize(new { title, body, url }); await client.SendNotificationAsync(subscription, payload, _vapid); } }

8. Background Sync — Xử lý offline form submit

Khi user submit form mà đang offline, Background Sync sẽ queue request và tự động gửi lại khi có mạng — user không cần thao tác lại:

// Trong Vue component async function submitOrder(order: Order) { try { await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(order) }) } catch { // Offline: đăng ký sync task const registration = await navigator.serviceWorker.ready await registration.sync.register('sync-orders') // Lưu data vào IndexedDB để SW đọc sau await saveToIndexedDB('pending-orders', order) } } // Trong Service Worker self.addEventListener('sync', (event) => { if (event.tag === 'sync-orders') { event.waitUntil(syncPendingOrders()) } }) async function syncPendingOrders() { const orders = await getFromIndexedDB('pending-orders') for (const order of orders) { await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(order) }) await removeFromIndexedDB('pending-orders', order.id) } }

9. PWA với Nuxt 4

Nuxt 4 hỗ trợ PWA thông qua module @vite-pwa/nuxt, kết hợp SSR/SSG với Service Worker một cách tự nhiên:

// nuxt.config.ts export default defineNuxtConfig({ modules: ['@vite-pwa/nuxt'], pwa: { registerType: 'autoUpdate', manifest: { name: 'Nuxt PWA App', short_name: 'NuxtPWA', theme_color: '#e94560', display: 'standalone', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png' }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png' } ] }, workbox: { navigateFallback: '/', globPatterns: ['**/*.{js,css,html,png,svg,ico}'] }, client: { installPrompt: true } } })

10. So sánh PWA vs Native App vs Hybrid

Tiêu chíPWANative AppHybrid (Capacitor)
Phát triển1 codebase (Web)2+ codebase (iOS + Android)1 codebase + native bridge
Phân phốiURL + Install PromptApp Store / Play StoreApp Store + Web
OfflineService Worker cacheFull native storageHybrid
Push NotificationWeb Push (mọi platform 2026)APNs / FCM nativeNative push
Hardware AccessCamera, GPS, Bluetooth (hạn chế)Toàn bộQua plugin
Hiệu năngTốt (Wasm boost)Tốt nhấtGần native
Chi phí phát triểnThấpCaoTrung bình
Cập nhậtTức thì (SW update)Qua App Store reviewHybrid

Khi nào chọn PWA?

PWA phù hợp nhất khi: ứng dụng chủ yếu hiển thị nội dung (e-commerce, tin tức, SaaS dashboard), cần reach rộng không qua app store, team đã có kỹ năng web (Vue/React), và không yêu cầu deep hardware access (NFC, Bluetooth LE phức tạp). Nếu cần xử lý nặng (game, video editing) hoặc hardware chuyên biệt — native hoặc hybrid là lựa chọn tốt hơn.

11. Performance Checklist cho PWA

  • Precache shell: App shell (HTML, CSS, JS chính) phải load từ cache — đảm bảo First Paint dưới 1 giây.
  • Lazy load routes: Dùng defineAsyncComponent và Vue Router lazy loading — chỉ tải code khi user navigate.
  • Image optimization: Serve AVIF/WebP, responsive images với srcset, và lazy loading với loading="lazy".
  • Cache API responses: Dùng NetworkFirst cho data hay thay đổi, CacheFirst cho data ít thay đổi.
  • Minimize SW scope: Chỉ cache những gì cần thiết — cache quá nhiều sẽ lãng phí storage và làm chậm SW install.
  • Navigation Preload: Bật navigationPreload để network request bắt đầu song song với SW startup, giảm latency cho trang đầu tiên.

12. Kết luận

PWA năm 2026 không còn là thử nghiệm — đó là chiến lược phân phối ứng dụng hiệu quả nhất cho phần lớn use case web. Với Vue 3 + Vite + vite-plugin-pwa, bạn có thể biến bất kỳ Vue app nào thành PWA chỉ trong vài phút, được hưởng lợi từ Service Worker caching, offline capability, push notification, và install prompt — tất cả với một codebase duy nhất.

Bắt đầu ngay

1. npm install -D vite-plugin-pwa → 2. Thêm VitePWA plugin vào vite.config.ts → 3. Tạo icon 192px + 512px → 4. Build và test với Lighthouse. Toàn bộ mất dưới 10 phút cho một Vue app có sẵn.

Nguồn tham khảo