Vượt xa trình duyệt: Xây dựng PWA bền bỉ với Service Worker

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Khoảng cách gây thất vọng giữa Web và Di động

Tất cả chúng ta đều đã từng trải qua: bạn đang đọc một bài báo trên tàu, tín hiệu mất trong vài giây, và đột nhiên bạn phải đối mặt với chú khủng long “Không có Internet”. Trong nhiều năm, đây là rào cản lớn đối với các ứng dụng web. Trong khi các ứng dụng di động bản địa (native apps) vẫn hoạt động ổn định khi mất kết nối, thì ứng dụng web đơn giản là “ngừng thở” nếu không có kết nối liên tục từ máy chủ.

Vấn đề bắt nguồn từ mô hình yêu cầu (request model) tiêu chuẩn của trình duyệt. Theo mặc định, mọi thao tác điều hướng hoặc làm mới đều buộc trình duyệt phải lấy tài nguyên trực tiếp từ mạng. Nếu kết nối thất bại, yêu cầu sẽ thất bại. Để khắc phục điều này, chúng ta cần một proxy nằm giữa trình duyệt và mạng. Đó chính xác là những gì Service Worker thực hiện.

Bắt đầu nhanh: Chuyển đổi trang web của bạn trong 5 phút

Việc chuyển đổi một trang web tiêu chuẩn thành Ứng dụng Web Tiến bộ (Progressive Web App – PWA) đòi hỏi hai thành phần cốt lõi: tệp manifest.json và một Service Worker đã được đăng ký. Theo kinh nghiệm của tôi, việc triển khai những yếu tố cơ bản này có thể giảm tỷ lệ thoát (bounce rate) lên tới 20% trên các mạng 3G chập chờn bằng cách đảm bảo giao diện người dùng (UI) tải ngay lập tức.

1. Tạo Web App Manifest

Manifest cho hệ điều hành biết ứng dụng của bạn nên hoạt động như thế nào khi được cài đặt. Nó ẩn thanh URL của trình duyệt và thiết lập màu sắc thương hiệu của bạn. Hãy tạo manifest.json trong thư mục gốc:

{
  "short_name": "FastApp",
  "name": "PWA Hiệu suất cao của tôi",
  "icons": [
    {
      "src": "/images/icon-192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/images/icon-512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/",
  "background_color": "#ffffff",
  "display": "standalone",
  "theme_color": "#2f55d4"
}

2. Đăng ký Service Worker

Thêm logic đăng ký này vào tệp JavaScript chính của bạn. Script này đóng vai trò là điểm đầu vào, thông báo cho trình duyệt biết tệp Service Worker của bạn nằm ở đâu.

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('Service Worker đã hoạt động!', reg.scope))
      .catch(err => console.error('Đăng ký thất bại:', err));
  });
}

3. Tạo file sw.js cơ bản

Bắt đầu với một tệp sw.js trống trong thư mục gốc. Thậm chí một tệp trống cũng đủ điều kiện để Chrome kích hoạt lời nhắc “Cài đặt ứng dụng”, miễn là bạn có manifest và HTTPS đang hoạt động.

Làm chủ Vòng đời của Service Worker

Service Worker là một Web Worker chuyên dụng. Nó chạy ngầm, hoạt động độc lập với luồng chính (main thread) và không thể can thiệp vào DOM. Vì nó tồn tại bên ngoài vòng đời trang web thông thường, nó hoạt động khác với các script tiêu chuẩn.

Giai đoạn Cài đặt (Installation)

Sự kiện install kích hoạt khi trình duyệt lần đầu tiên phát hiện ra script hoặc thấy sự thay đổi từng byte trong tệp. Đây là thời điểm tốt nhất để lưu trữ “App Shell” vào bộ nhớ đệm (cache). App Shell bao gồm HTML, CSS và các file JS cốt lõi cần thiết để giao diện hiển thị mà không cần mạng.

const CACHE_NAME = 'v1_static_assets';
const ASSETS = [
  '/',
  '/index.html',
  '/css/style.css',
  '/js/app.js',
  '/images/logo.png'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      return cache.addAll(ASSETS);
    })
  );
});

Giai đoạn Kích hoạt (Activation)

Quá trình kích hoạt xảy ra sau khi Service Worker cũ được giải phóng và Service Worker mới nắm quyền kiểm soát. Hãy sử dụng giai đoạn này để dọn dẹp các cache đã lỗi thời. Điều này giúp thiết bị của người dùng không bị đầy bộ nhớ bởi các file CSS cũ hoặc các tệp phiên bản cũ.

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(keys
        .filter(key => key !== CACHE_NAME)
        .map(key => caches.delete(key))
      );
    })
  );
});

Sự kiện Fetch: Chặn các yêu cầu

Đây là “buồng máy” của PWA. Mọi yêu cầu mạng mà ứng dụng thực hiện đều đi qua sự kiện này. Bạn có thể chọn trả về tệp từ cache, lấy từ web, hoặc thậm chí tạo phản hồi ngay tức thì.

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

Chọn chiến lược Caching phù hợp

Không phải dữ liệu nào cũng giống nhau. Logo công ty hiếm khi thay đổi, nhưng bảng giá chứng khoán thì cập nhật từng giây. Sử dụng sai chiến lược sẽ dẫn đến dữ liệu cũ hoặc các vòng xoay tải trang không cần thiết.

Cache First (Tốt nhất cho tài nguyên tĩnh)

Sử dụng chiến lược này cho font chữ, icon và stylesheet. Ứng dụng sẽ kiểm tra cache ngay lập tức. Nếu tệp có sẵn, nó sẽ tải trong vài mili giây. Nó chỉ truy cập mạng nếu cache trống.

Network First (Tốt nhất cho dữ liệu động)

Đây là lựa chọn hàng đầu cho các lệnh gọi API hoặc bảng tin tức. Ứng dụng sẽ thử kết nối mạng trước để lấy dữ liệu mới nhất. Nếu người dùng đang ở trong hầm hoặc dùng Wi-Fi yếu, nó sẽ quay lại sử dụng phiên bản được lưu trong cache gần nhất.

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          const resClone = response.clone();
          caches.open('dynamic-data').then(cache => cache.put(event.request, resClone));
          return response;
        })
        .catch(() => caches.match(event.request))
    );
  }
});

Stale-While-Revalidate

Đây là “tiêu chuẩn vàng” cho môi trường production. Nó trả về phiên bản trong cache ngay lập tức để người dùng thấy nội dung ngay. Đồng thời, nó sẽ lấy bản cập nhật ở chế độ chạy ngầm và thay thế cho lần truy cập tiếp theo.

Lời khuyên thực tế cho môi trường Production

Xây dựng PWA thì đơn giản, nhưng để làm cho nó hoạt động hoàn hảo thì cần chú ý đến chi tiết. Dưới đây là bốn bài học rút ra từ việc triển khai thực tế:

  • HTTPS là bắt buộc: Service Worker có thể chặn tất cả lưu lượng mạng. Vì quyền năng này, các trình duyệt yêu cầu kết nối HTTPS an toàn (ngoại trừ localhost khi thử nghiệm).
  • Quản lý cập nhật cẩn thận: Trình duyệt kiểm tra các bản cập nhật sw.js sau mỗi 24 giờ. Nếu có phiên bản mới, nó sẽ đợi cho đến khi tất cả các tab đang mở sử dụng phiên bản cũ được đóng lại. Bạn có thể sử dụng self.skipWaiting() để ép buộc cập nhật, nhưng hãy cẩn thận để không làm gián đoạn phiên làm việc hiện tại của người dùng.
  • Lợi thế từ DevTools: Sử dụng tab Application trong Chrome DevTools. Nó cho phép bạn mô phỏng chế độ offline, ép buộc cập nhật worker và kiểm tra từng bộ nhớ cache riêng lẻ.
  • Hướng tới điểm Lighthouse 100/100: Chạy kiểm tra Lighthouse trong Chrome. Nó kiểm tra manifest, các icon hợp lệ và liệu trang web của bạn có duy trì được khả năng tương tác ở tốc độ 3G hay không.

Việc triển khai Service Worker giúp chuyển đổi kiến trúc của bạn từ mô hình “luôn trực tuyến” mong manh sang tư duy “ưu tiên ngoại tuyến” (offline-first). Bằng cách coi mạng là một tiện ích bổ sung tùy chọn thay vì một yêu cầu bắt buộc, bạn sẽ cung cấp một trải nghiệm nhanh chóng, đáng tin cậy bất kể vị trí của người dùng.

Share: