Tải ứng dụng web tức thì với cấu trúc shell ứng dụng

Addy Osmani
Addy Osmani

Giao diện ứng dụng là HTML, CSS và JavaScript tối thiểu hỗ trợ giao diện người dùng. Giao diện ứng dụng nên:

  • tải nhanh
  • được lưu vào bộ nhớ đệm
  • nội dung hiển thị linh động

Giao diện ứng dụng là bí quyết để có hiệu suất tốt đáng tin cậy. Hãy xem giao diện người dùng của ứng dụng giống như bộ mã bạn sẽ xuất bản lên cửa hàng ứng dụng nếu bạn đang xây dựng một ứng dụng gốc. Đó là tải cần thiết để bắt đầu, nhưng có thể không phải là toàn bộ. Tệp này giữ cho giao diện người dùng cục bộ và tự động lấy nội dung thông qua một API.

Phân tách giao diện người dùng của giao diện HTML, JS và CSS và nội dung HTML

Thông tin khái quát

Bài viết về Ứng dụng web tiến bộ của Alex Russell mô tả cách ứng dụng web có thể thay đổi dần thông qua việc sử dụng và sự đồng ý của người dùng nhằm cung cấp trải nghiệm giống ứng dụng gốc với tính năng hỗ trợ ngoại tuyến, thông báo đẩy và khả năng được thêm vào màn hình chính. Điều này phụ thuộc rất nhiều vào chức năng và lợi ích về hiệu suất của service worker cũng như khả năng lưu vào bộ nhớ đệm của chúng. Điều này cho phép bạn tập trung vào tốc độ, cung cấp cho ứng dụng web của bạn cùng một tính năng tải tức thì và cập nhật thường xuyên mà bạn thường thấy trong các ứng dụng gốc.

Để tận dụng tối đa các tính năng này, chúng ta cần một cách suy nghĩ mới về trang web: kiến trúc giao diện ứng dụng.

Hãy tìm hiểu cách định cấu trúc ứng dụng bằng kiến trúc giao diện ứng dụng tăng cường của trình chạy dịch vụ. Chúng ta sẽ xem xét cả hiển thị phía máy khách và phía máy chủ, đồng thời chia sẻ một mẫu toàn diện mà bạn có thể dùng thử ngay hôm nay.

Để nhấn mạnh điểm này, ví dụ bên dưới cho thấy lần tải đầu tiên của ứng dụng sử dụng cấu trúc này. Để ý thông báo ngắn "Ứng dụng đã sẵn sàng để dùng khi không có mạng" ở cuối màn hình. Nếu sau này có bản cập nhật cho shell, chúng ta có thể thông báo cho người dùng để làm mới phiên bản mới.

Hình ảnh trình chạy dịch vụ chạy trong Công cụ cho nhà phát triển cho giao diện ứng dụng

Một lần nữa, Service worker là gì?

Service worker là một tập lệnh chạy trong nền, tách biệt với trang web của bạn. Dịch vụ này phản hồi các sự kiện, bao gồm cả yêu cầu về mạng được tạo từ các trang mà dịch vụ này phân phát và đẩy thông báo từ máy chủ của bạn. Trình chạy dịch vụ có thời gian tồn tại ngắn có chủ đích. Nó sẽ thức dậy khi nhận được một sự kiện và chỉ chạy khi cần xử lý sự kiện đó.

Service worker cũng có một bộ API hạn chế khi so sánh với JavaScript trong ngữ cảnh duyệt web thông thường. Đây là tiêu chuẩn dành cho nhân viên trên web. Trình chạy dịch vụ không thể truy cập vào DOM nhưng có thể truy cập vào các mục như API Bộ nhớ đệm, đồng thời có thể đưa ra yêu cầu mạng bằng cách sử dụng API Tìm nạp. Bạn cũng có thể dùng IndexedDB APIpostMessage() để cố định dữ liệu và nhắn tin giữa trình chạy dịch vụ và các trang mà trình chạy dịch vụ kiểm soát. Sự kiện đẩy được gửi từ máy chủ của bạn có thể gọi API thông báo để tăng mức độ tương tác của người dùng.

Trình chạy dịch vụ có thể chặn các yêu cầu mạng được thực hiện từ một trang (sự kiện này sẽ kích hoạt sự kiện tìm nạp trên trình chạy dịch vụ) và trả về phản hồi được truy xuất từ mạng hoặc truy xuất từ bộ nhớ đệm cục bộ hay thậm chí được tạo theo phương thức lập trình. Đây thực sự là một proxy có thể lập trình trong trình duyệt. Tóm tắt là cho dù phản hồi đến từ đâu, thì kết quả sẽ hiển thị trên trang web như thể không có sự tham gia của nhân viên dịch vụ.

Để tìm hiểu sâu hơn về trình chạy dịch vụ, hãy đọc phần Giới thiệu về Trình chạy dịch vụ.

Lợi ích về hiệu suất

Trình chạy dịch vụ rất hiệu quả trong việc lưu vào bộ nhớ đệm khi không có kết nối mạng, nhưng chúng cũng mang lại hiệu suất đáng kể dưới dạng tải tức thì cho các lượt truy cập lặp lại vào trang web hoặc ứng dụng web của bạn. Bạn có thể lưu vào bộ nhớ đệm cho giao diện ứng dụng để giao diện ứng dụng hoạt động khi không có mạng và điền nội dung bằng JavaScript.

Trong các lượt truy cập lặp lại, điều này cho phép bạn nhận các pixel có ý nghĩa trên màn hình mà không cần mạng, ngay cả khi nội dung của bạn cuối cùng cũng đến từ đó. Bạn có thể coi việc này là hiển thị thanh công cụ và thẻ ngay lập tức, sau đó tải phần nội dung còn lại dần dần.

Để kiểm tra kiến trúc này trên thiết bị thực, chúng tôi đã chạy mẫu shell ứng dụng trên WebPageTest.org và hiển thị kết quả bên dưới.

Thử nghiệm 1: Thử nghiệm trên cáp bằng Nexus 5 bằng Chrome Dev

Khung hiển thị đầu tiên của ứng dụng phải tìm nạp tất cả tài nguyên từ mạng và không hiển thị được ý nghĩa cho đến 1,2 giây. Nhờ việc lưu vào bộ nhớ đệm của trình chạy dịch vụ, lượt truy cập lặp lại của chúng tôi sẽ hiển thị có ý nghĩa và hoàn tất việc tải hoàn toàn trong 0,5 giây.

Sơ đồ hiển thị kiểm tra trang web cho kết nối cáp

Thử nghiệm 2: Thử nghiệm 3G bằng Nexus 5 bằng Chrome Dev

Chúng ta cũng có thể kiểm tra mẫu bằng kết nối 3G chậm hơn một chút. Lần này mất 2,5 giây trong lần truy cập đầu tiên để hiển thị ý nghĩa đầu tiên của chúng tôi. Cần 7,1 giây để tải hoàn toàn trang. Với tính năng lưu vào bộ nhớ đệm của trình chạy dịch vụ, lượt truy cập lặp lại của chúng ta sẽ hiển thị có ý nghĩa và hoàn tất quá trình tải trong 0,8 giây.

Sơ đồ hiển thị thử nghiệm trang web cho kết nối 3G

Các lượt xem khác cũng kể về câu chuyện tương tự. So sánh 3 giây cần để đạt được màu vẽ có ý nghĩa đầu tiên trong giao diện ứng dụng:

Vẽ dòng thời gian cho chế độ xem đầu tiên từ Kiểm tra trang web

0,9 giây khi cùng một trang được tải từ bộ nhớ đệm của trình chạy dịch vụ. Người dùng cuối của chúng tôi tiết kiệm được hơn 2 giây thời gian.

Vẽ dòng thời gian cho lượt xem lặp lại từ Kiểm tra trang web

Các ứng dụng của riêng bạn sử dụng kiến trúc shell ứng dụng có thể đạt được hiệu suất tương tự và đáng tin cậy.

Service worker có đòi hỏi chúng ta suy nghĩ lại cách chúng ta cấu trúc ứng dụng không?

Service worker ngụ ý một số thay đổi tinh tế trong kiến trúc ứng dụng. Thay vì nén tất cả ứng dụng của bạn vào một chuỗi HTML, việc thực hiện theo kiểu AJAX có thể mang lại lợi ích. Đây là nơi bạn có một shell (luôn được lưu vào bộ nhớ đệm và luôn có thể khởi động mà không cần mạng) và nội dung được làm mới thường xuyên và được quản lý riêng biệt.

Những tác động của sự phân chia này là rất lớn. Trong lần truy cập đầu tiên, bạn có thể kết xuất nội dung trên máy chủ và cài đặt trình chạy dịch vụ trên máy khách. Trong các lần truy cập tiếp theo, bạn chỉ cần yêu cầu dữ liệu.

Tính năng nâng cao tăng dần thì sao?

Mặc dù trình chạy dịch vụ hiện không được tất cả trình duyệt hỗ trợ, nhưng cấu trúc shell nội dung ứng dụng sử dụng nâng cao tăng dần để đảm bảo mọi người đều có thể truy cập nội dung. Ví dụ: hãy lấy dự án mẫu của chúng ta.

Dưới đây, bạn có thể xem phiên bản đầy đủ được hiển thị trong Chrome, Firefox Nightly và Safari. Ở phía bên trái, bạn có thể thấy phiên bản Safari, trong đó nội dung được hiển thị trên máy chủ mà không có trình chạy dịch vụ. Ở bên phải, chúng ta thấy các phiên bản Chrome và Firefox Nightly do trình chạy dịch vụ cung cấp.

Hình ảnh Giao diện ứng dụng được tải trong Safari, Chrome và Firefox

Khi nào nên sử dụng kiến trúc này?

Kiến trúc giao diện ứng dụng phù hợp nhất với các ứng dụng và trang web động. Nếu trang web của bạn nhỏ và tĩnh, thì bạn có thể không cần một shell ứng dụng mà chỉ cần lưu toàn bộ trang web vào bộ nhớ đệm trong bước oninstall của trình chạy dịch vụ. Sử dụng phương pháp phù hợp nhất với dự án của bạn. Một số khung JavaScript đã khuyến khích việc chia tách logic ứng dụng khỏi nội dung, giúp mẫu này dễ áp dụng hơn.

Có ứng dụng phát hành công khai nào đang sử dụng mẫu này không?

Chỉ cần một vài thay đổi đối với giao diện người dùng tổng thể của ứng dụng là có thể có kiến trúc giao diện ứng dụng và đã hoạt động tốt trên các trang web có quy mô lớn như Ứng dụng web tiến bộ I/O 2015 của Google và Hộp thư đến của Google.

Hình ảnh Hộp thư đến của Google đang tải. Hình minh hoạ Hộp thư đến bằng trình chạy dịch vụ.

Giao diện người dùng của ứng dụng ngoại tuyến là một lợi thế lớn về hiệu suất và cũng được thể hiện rõ trong ứng dụng Wikipedia ngoại tuyến của Jake Archibald và ứng dụng web tiến bộ của Flipkart Lite.

Ảnh chụp màn hình bản minh hoạ Wikipedia của Jake Archibald.

Giải thích cấu trúc

Trong trải nghiệm tải đầu tiên, mục tiêu của bạn là đưa nội dung có ý nghĩa lên màn hình của người dùng càng nhanh càng tốt.

Tải lần đầu và tải các trang khác

Sơ đồ về lượt tải đầu tiên bằng App Shell

Nhìn chung, cấu trúc giao diện ứng dụng sẽ:

  • Ưu tiên tải ban đầu, nhưng cho phép trình chạy dịch vụ lưu giao diện ứng dụng vào bộ nhớ đệm để các lượt truy cập lặp lại không yêu cầu tìm nạp lại shell từ mạng.

  • Tải từng phần hoặc tải trong nền mọi thứ khác. Một lựa chọn phù hợp là sử dụng tính năng đọc qua bộ nhớ đệm đối với nội dung động.

  • Sử dụng các công cụ của trình chạy dịch vụ, chẳng hạn như sw-precache, để lưu vào bộ nhớ đệm và cập nhật trình chạy dịch vụ một cách đáng tin cậy nhằm quản lý nội dung tĩnh của bạn. (Tìm hiểu thêm về sw-precache sau.)

Để đạt được điều này:

  • Máy chủ sẽ gửi nội dung HTML mà máy khách có thể hiển thị và sử dụng các tiêu đề hết hạn bộ nhớ đệm HTTP trong tương lai xa để tính đến các trình duyệt không hỗ trợ trình chạy dịch vụ. Công cụ này sẽ phân phát tên tệp bằng hàm băm để kích hoạt cả việc tạo phiên bản và dễ dàng cập nhật sau này trong vòng đời của ứng dụng.

  • (Các) Trang sẽ bao gồm các kiểu CSS cùng dòng trong thẻ <style> trong tài liệu <head> để cung cấp lớp vẽ đầu tiên nhanh chóng cho giao diện ứng dụng. Mỗi trang sẽ tải không đồng bộ JavaScript cần thiết cho chế độ xem hiện tại. Vì CSS không thể tải không đồng bộ, nên chúng tôi có thể yêu cầu kiểu bằng cách sử dụng JavaScript vì nó không đồng bộ chứ không phải được phân tích cú pháp và đồng bộ. Chúng ta cũng có thể tận dụng requestAnimationFrame() để tránh trường hợp chúng tôi nhận được kết quả tìm kiếm nhanh trong bộ nhớ đệm và kết quả là các kiểu vô tình trở thành một phần của đường dẫn hiển thị quan trọng. requestAnimationFrame() buộc vẽ khung đầu tiên trước khi tải kiểu. Một lựa chọn khác là sử dụng các dự án như loadCSS của Filament Group để yêu cầu CSS không đồng bộ bằng JavaScript.

  • Service worker sẽ lưu trữ một mục nhập đã lưu vào bộ nhớ đệm của shell ứng dụng để trong các lần truy cập lặp lại, shell có thể được tải hoàn toàn từ bộ nhớ đệm của Service worker, trừ khi có bản cập nhật trên mạng.

Giao diện ứng dụng cho nội dung

Cách triển khai thực tế

Chúng tôi đã viết một mẫu hoàn chỉnh bằng cách sử dụng cấu trúc giao diện ứng dụng, JavaScript vanilla ES2015 cho ứng dụng và Express.js cho máy chủ. Tất nhiên là bạn không có gì cản trở việc sử dụng ngăn xếp của riêng mình cho các phần phần ứng dụng hoặc máy chủ (ví dụ: PHP, Ruby, Python).

Vòng đời của trình chạy dịch vụ

Đối với dự án giao diện ứng dụng, chúng ta sử dụng sw-precache, cung cấp vòng đời của trình chạy dịch vụ sau đây:

Sự kiện Hành động
Cài đặt Lưu giao diện ứng dụng và các tài nguyên ứng dụng trang đơn khác vào bộ nhớ đệm.
Ứng dụng Xoá bộ nhớ đệm cũ.
Tìm nạp Phân phối ứng dụng web một trang cho URL và sử dụng bộ nhớ đệm cho nội dung và các phần được xác định trước. Sử dụng mạng cho các yêu cầu khác.

Bit máy chủ

Trong cấu trúc này, thành phần phía máy chủ (trong trường hợp của chúng ta là thành phần được viết ở dạng Express) có thể xử lý riêng biệt nội dung và bản trình bày. Nội dung có thể được thêm vào một bố cục HTML để làm cho trang hiển thị tĩnh, hoặc nội dung đó có thể được phân phát riêng và tải động.

Nói một cách dễ hiểu, cách thiết lập phía máy chủ của bạn có thể khác biệt đáng kể so với cách chúng tôi sử dụng cho ứng dụng minh hoạ. Hầu hết thiết lập máy chủ đều có thể thực hiện được mẫu ứng dụng web này, mặc dù hầu hết thiết lập máy chủ cần thiết kế lại một chút. Chúng tôi nhận thấy rằng mô hình sau hoạt động khá tốt:

Sơ đồ về Cấu trúc giao diện ứng dụng (App Shell)
  • Điểm cuối được xác định cho ba phần của ứng dụng của bạn: (chỉ mục/ký tự đại diện) của URL mà người dùng nhìn thấy, giao diện ứng dụng (trình chạy dịch vụ) và phần HTML của bạn.

  • Mỗi điểm cuối có một bộ điều khiển lấy bố cục thanh điều khiển, có thể kéo thành phần hiển thị và một phần của tay điều khiển. Nói một cách đơn giản, một phần là các chế độ xem là các đoạn HTML được sao chép vào trang cuối cùng. Lưu ý: Các khung JavaScript thực hiện đồng bộ hoá dữ liệu nâng cao hơn thường dễ chuyển đổi sang cấu trúc Application Shell. Các ứng dụng này có xu hướng sử dụng tính năng liên kết và đồng bộ hoá dữ liệu thay vì chỉ dùng một phần.

  • Ban đầu, người dùng sẽ thấy một trang tĩnh có nội dung. Trang này sẽ đăng ký một trình chạy dịch vụ, nếu được hỗ trợ, trình này sẽ lưu giao diện ứng dụng vào bộ nhớ đệm và mọi thứ phụ thuộc vào (CSS, JS, v.v.).

  • Sau đó, giao diện ứng dụng sẽ hoạt động như một ứng dụng web một trang, sử dụng javascript để XHR trong nội dung cho một URL cụ thể. Các lệnh gọi XHR được thực hiện đến điểm cuối /partials*. Điểm cuối này trả về phần nhỏ HTML, CSS và JS cần thiết để hiển thị nội dung đó. Lưu ý: Có nhiều cách để tiếp cận vấn đề này và XHR chỉ là một trong số đó. Một số ứng dụng sẽ chèn dữ liệu cùng dòng (có thể bằng JSON) để kết xuất ban đầu và do đó không phải là "tĩnh" trong nghĩa HTML được làm phẳng.

  • Những trình duyệt không có hỗ trợ trình chạy dịch vụ phải luôn được cung cấp trải nghiệm dự phòng. Trong phần minh hoạ, chúng ta sẽ quay lại phương thức kết xuất tĩnh cơ bản phía máy chủ, nhưng đây chỉ là một trong nhiều lựa chọn. Khía cạnh trình chạy dịch vụ mang lại cho bạn các cơ hội mới để nâng cao hiệu suất của ứng dụng kiểu Ứng dụng trang đơn bằng cách sử dụng giao diện ứng dụng được lưu vào bộ nhớ đệm.

Tạo phiên bản tệp

Một câu hỏi được đặt ra là cách xử lý việc tạo phiên bản và cập nhật tệp. Đây là nội dung dành riêng cho ứng dụng và có các tuỳ chọn như sau:

  • Ưu tiên mạng. Nếu không, hãy sử dụng phiên bản đã lưu vào bộ nhớ đệm.

  • Chỉ kết nối mạng và không kết nối được nếu không có mạng.

  • Lưu phiên bản cũ vào bộ nhớ đệm rồi cập nhật sau.

Đối với chính giao diện ứng dụng, bạn nên sử dụng phương pháp ưu tiên bộ nhớ đệm để thiết lập trình chạy dịch vụ. Nếu bạn không lưu giao diện ứng dụng vào bộ nhớ đệm, tức là bạn chưa áp dụng cấu trúc đúng cách.

Công cụ

Chúng tôi duy trì một số thư viện trình trợ giúp trình chạy dịch vụ khác nhau để giúp quá trình lưu trước shell của ứng dụng vào bộ nhớ đệm hoặc xử lý các mẫu lưu vào bộ nhớ đệm thông thường dễ thiết lập hơn.

Ảnh chụp màn hình trang web Thư viện trình chạy dịch vụ trên trang Kiến thức cơ bản về web

Sử dụng sw-precache cho giao diện ứng dụng của bạn

Sử dụng sw-precache để lưu giao diện ứng dụng vào bộ nhớ đệm sẽ xử lý những mối lo ngại về sửa đổi tệp, các câu hỏi về cài đặt/kích hoạt và trường hợp tìm nạp cho giao diện ứng dụng. Thả sw-precache vào quy trình xây dựng của ứng dụng và dùng các ký tự đại diện có thể định cấu hình để chọn tài nguyên tĩnh. Thay vì tự tạo tập lệnh trình chạy dịch vụ của bạn theo cách thủ công, hãy để sw-precache tạo một tập lệnh giúp quản lý bộ nhớ đệm của bạn một cách an toàn và hiệu quả, sử dụng trình xử lý tìm nạp ưu tiên bộ nhớ đệm.

Các lượt truy cập đầu tiên vào ứng dụng của bạn sẽ kích hoạt tính năng lưu trước toàn bộ tài nguyên cần thiết. Quá trình này tương tự như trải nghiệm cài đặt ứng dụng gốc qua cửa hàng ứng dụng. Khi người dùng quay lại ứng dụng, chỉ những tài nguyên đã cập nhật mới được tải xuống. Trong bản minh hoạ, chúng tôi thông báo cho người dùng khi có giao diện người dùng mới kèm theo thông báo "Bản cập nhật ứng dụng. Hãy làm mới để tải phiên bản mới." Mẫu này là một cách nhanh chóng để cho người dùng biết họ có thể làm mới phiên bản mới nhất.

Dùng hộp công cụ sw để lưu vào bộ nhớ đệm trong thời gian chạy

Dùng sw-toolbox để lưu vào bộ nhớ đệm trong thời gian chạy với nhiều chiến lược tuỳ thuộc vào tài nguyên:

  • cacheFirst cho hình ảnh, cùng với một bộ nhớ đệm có tên chuyên dụng có chính sách hết hạn tuỳ chỉnh là N maxEntries.

  • networkFirst hoặc nhanh nhất đối với các yêu cầu API, tuỳ thuộc vào độ mới của nội dung mong muốn. Nhanh nhất có thể sẽ ổn, nhưng nếu có một nguồn cấp dữ liệu API cụ thể được cập nhật thường xuyên, hãy sử dụng networkFirst.

Kết luận

Kiến trúc giao diện ứng dụng mang lại một số lợi ích nhưng chỉ phù hợp với một số lớp ứng dụng. Mô hình này vẫn còn rất trẻ nên sẽ rất đáng để đánh giá nỗ lực cũng như lợi ích về hiệu suất tổng thể của kiến trúc này.

Trong các thử nghiệm của mình, chúng tôi đã tận dụng tính năng chia sẻ mẫu giữa ứng dụng và máy chủ để giảm thiểu công việc tạo 2 lớp ứng dụng. Điều này đảm bảo tính năng nâng cao tăng dần vẫn là tính năng cốt lõi.

Nếu bạn đang cân nhắc sử dụng trình chạy dịch vụ trong ứng dụng của mình, hãy xem cấu trúc và đánh giá xem cấu trúc đó có phù hợp với dự án của bạn hay không.

Nhờ những người đánh giá của chúng tôi: Jeff Posnick, Paul Lewis, Alex Russell, Seth Thompson, Rob Dodson, Taylor Savage và Joe Medley.