Xây dựng Ứng dụng web tiến bộ Google I/O 2016

Sân nhà ở Virginia

Tóm tắt

Tìm hiểu cách chúng tôi tạo ra một ứng dụng trang đơn bằng các thành phần web, Polymer và Material Design, đồng thời ra mắt ứng dụng đó trên Google.com.

Kết quả

  • Tương tác nhiều hơn so với ứng dụng gốc (4:06 phút của web dành cho thiết bị di động so với 2:40 phút của Android).
  • Thời gian hiển thị đầu tiên nhanh hơn 450 mili giây đối với người dùng cũ nhờ chức năng lưu vào bộ nhớ đệm của trình chạy dịch vụ
  • 84% khách truy cập đã hỗ trợ Service Worker
  • Số lượt thêm vào màn hình chính tăng hơn 900% so với năm 2015.
  • 3,8% người dùng đã ngoại tuyến nhưng vẫn tiếp tục tạo ra 11 nghìn lượt xem trang!
  • 50% người dùng đã đăng nhập bật thông báo.
  • 536 nghìn thông báo đã được gửi đến người dùng (12% đưa họ trở lại).
  • 99% trình duyệt của người dùng đã hỗ trợ đoạn mã polyfill trong thành phần web

Tổng quan

Năm nay, tôi rất vui khi được làm việc trên ứng dụng web tiến bộ Google I/O 2016, được đặt tên trìu mến là "IOWA". Trò chơi này ưu tiên cho thiết bị di động, hoạt động hoàn toàn ngoại tuyến và được truyền cảm hứng rất nhiều từ Material Design.

IOWA là một ứng dụng trang đơn (SPA) được xây dựng bằng các thành phần web, Polymer và Firebase, đồng thời có phần phụ trợ mở rộng được viết bằng App Engine (Go). Google lưu trước vào bộ nhớ đệm nội dung bằng cách sử dụng service worker, tự động tải trang mới, chuyển đổi linh hoạt giữa các chế độ xem và sử dụng lại nội dung sau lần tải đầu tiên.

Trong nghiên cứu điển hình này, tôi sẽ trình bày một số quyết định thú vị hơn về cấu trúc mà chúng tôi đã đưa ra cho giao diện người dùng. Nếu bạn quan tâm đến mã nguồn, hãy xem mã đó trên GitHub.

Xem trên GitHub

Xây dựng SPA bằng các thành phần web

Mỗi trang là một thành phần

Một trong những khía cạnh cốt lõi về giao diện người dùng của chúng tôi là giao diện người dùng tập trung vào các thành phần web. Trên thực tế, mỗi trang trong SPA của chúng tôi là một thành phần web:

    <io-home-page date="2016-05-18T17:00:00Z" app="[[app]]"></io-home-page>
    <io-schedule-page date="2016-05-18T17:00:00Z" app="{ % templatetag openvariable % }app}}"></io-schedule-page>
    <io-attend-page></io-attend-page>
    <io-extended-page></io-extended-page>
    <io-faq-page></io-faq-page>

Vì sao chúng tôi làm việc này? Lý do đầu tiên là mã này có thể đọc được. Khi mới đọc lần đầu, bạn sẽ biết rõ mọi trang trong ứng dụng của mình. Lý do thứ hai là các thành phần web có một số đặc tính hữu ích để xây dựng SPA. Rất nhiều vấn đề thường gặp (quản lý trạng thái, kích hoạt chế độ xem, xác định phạm vi kiểu) sẽ biến mất nhờ các tính năng vốn có của phần tử <template>, Phần tử tuỳ chỉnhDOM tối. Đây là các công cụ cho nhà phát triển được tích hợp vào trình duyệt. Tại sao bạn không khai thác chúng?

Bằng cách tạo một Phần tử tuỳ chỉnh cho mỗi trang, chúng tôi đã nhận được rất nhiều miễn phí:

  • Quản lý vòng đời của trang.
  • Giới hạn phạm vi của CSS/HTML đối với trang.
  • Tất cả CSS/HTML/JS dành riêng cho một trang đều được nhóm lại và tải lại với nhau khi cần.
  • Bạn có thể sử dụng lại các khung hiển thị. Vì các trang là nút DOM, chỉ cần thêm hoặc xóa các trang đó sẽ làm thay đổi chế độ xem.
  • Các nhà bảo trì trong tương lai có thể hiểu ứng dụng của chúng ta chỉ bằng cách dò tìm mã đánh dấu.
  • Thẻ đánh dấu do máy chủ kết xuất có thể được cải tiến dần khi các định nghĩa phần tử được đăng ký và nâng cấp bởi trình duyệt.
  • Phần tử tuỳ chỉnh có mô hình kế thừa. Mã DRY là mã tốt.
  • ...nhiều nội dung khác.

Chúng tôi đã tận dụng tối đa những lợi ích này trong IOWA. Hãy cùng đi sâu vào một số chi tiết.

Kích hoạt trang một cách linh động

Phần tử <template> là phương thức tiêu chuẩn của trình duyệt để tạo mã đánh dấu có thể sử dụng lại. <template> có hai đặc điểm mà SPA có thể tận dụng. Trước tiên, mọi nội dung bên trong <template> đều sẽ được trì hoãn cho đến khi một thực thể của mẫu được tạo. Thứ hai, trình duyệt phân tích cú pháp mã đánh dấu nhưng không truy cập được nội dung từ trang chính. Đây là một phân đoạn đánh dấu thực sự và có thể sử dụng lại. Ví dụ:

<template id="t">
    <div>This markup is inert and not part of the main page's DOM.</div>
    <img src="profile.png"> <!-- not loaded by the browser -->
    <video id="vid" src="vid.mp4"></video> <!-- doesn't load/start -->
    <script>alert("Not run until the template is stamped");</script>
</template>

Polyme extends <template> với một số phần tử tuỳ chỉnh tiện ích loại, cụ thể là <template is="dom-if"><template is="dom-repeat">. Cả hai đều là phần tử tuỳ chỉnh mở rộng <template> với các tính năng bổ sung. Ngoài ra, nhờ tính chất khai báo của các thành phần web, cả hai thành phần này đều hoạt động chính xác những gì bạn mong đợi. Mã đánh dấu thành phần đầu tiên dựa trên điều kiện. Mã đánh dấu thứ hai lặp lại cho mọi mục trong danh sách (mô hình dữ liệu).

IOWA sử dụng các phần tử mở rộng loại này như thế nào?

Nếu bạn còn nhớ, mỗi trang trong IOWA đều là một thành phần web. Tuy nhiên, việc khai báo mọi thành phần trong lần tải đầu tiên sẽ là ngớ ngẩn. Điều đó có nghĩa là bạn nên tạo một bản sao của mỗi trang khi ứng dụng tải lần đầu tiên. Chúng tôi không muốn ảnh hưởng đến hiệu suất tải ban đầu, đặc biệt là khi một số người dùng chỉ chuyển đến 1 hoặc 2 trang.

Giải pháp của chúng tôi là gian lận. Trong IOWA, chúng ta gói phần tử của mỗi trang trong một <template is="dom-if"> để nội dung của trang không tải trong lần khởi động đầu tiên. Sau đó, chúng tôi kích hoạt trang khi thuộc tính name của mẫu khớp với URL. Thành phần web <lazy-pages> xử lý toàn bộ logic này cho chúng ta. Mã đánh dấu sẽ có dạng như sau:

<!-- Lazy pages manages the template stamping. It watches for route changes
        and sets `template.if = true` on the appropriate template. -->
<lazy-pages>
    <template is="dom-if" name="home">
    <io-home-page date="2016-05-18T17:00:00Z"></io-home-page>
    </template>

    <template is="dom-if" name="schedule">
    <io-schedule-page date="2016-05-18T17:00:00Z"></io-schedule-page>
    </template>

    <template is="dom-if" name="attend">
    <io-attend-page></io-attend-page>
    </template>
</lazy-pages>

Điều tôi thích ở đây là mọi trang đều được phân tích cú pháp và sẵn sàng hoạt động khi trang tải nhưng CSS/HTML/JS của trang chỉ được thực thi theo yêu cầu (khi <template> mẹ của trang được đóng dấu). Khung hiển thị động + lazy sử dụng các thành phần web FTW.

Các điểm cải tiến trong tương lai

Khi trang tải lần đầu tiên, chúng tôi sẽ tải tất cả các lượt Nhập HTML cho từng trang cùng một lúc. Một cách cải thiện rõ ràng là chỉ tải từng phần các định nghĩa phần tử khi cần thiết. Polymer cũng có một trình trợ giúp hữu ích để tải không đồng bộ các dữ liệu Nhập HTML:

Polymer.Base.importHref('io-home-page.html', (e) => { ... });

IOWA không làm điều này vì a) chúng tôi trở nên lười biếng và b) không rõ chúng tôi sẽ tăng hiệu suất đến mức nào. Lần sơn đầu tiên của chúng tôi mất khoảng 1 giây.

Quản lý vòng đời của trang

API Phần tử tuỳ chỉnh xác định "phương thức gọi lại trong vòng đời" để quản lý trạng thái của một thành phần. Khi triển khai các phương thức này, bạn sẽ nhận được các điểm hấp dẫn miễn phí về vòng đời của một thành phần:

createdCallback() {
    // automatically called when an instance of the element is created.
}

attachedCallback() {
    // automatically called when the element is attached to the DOM.
}

detachedCallback() {
    // automatically called when the element is removed from the DOM.
}

attributeChangedCallback() {
    // automatically called when an HTML attribute changes.
}

Bạn có thể dễ dàng tận dụng các lệnh gọi lại này trong IOWA. Hãy nhớ rằng mỗi trang đều là một nút DOM độc lập. Để chuyển sang một "chế độ xem mới" trong SPA, bạn cần đính kèm một nút vào DOM và xoá một nút khác.

Chúng ta đã sử dụng attachedCallback để thực hiện công việc thiết lập (trạng thái khởi tạo, đính kèm trình nghe sự kiện). Khi người dùng chuyển đến một trang khác, detachedCallback sẽ dọn dẹp (xoá trình nghe, đặt lại trạng thái được chia sẻ). Chúng tôi cũng đã mở rộng các phương thức gọi lại trong vòng đời gốc bằng một số phương thức gọi lại của riêng mình:

onPageTransitionDone() {
    // page transition animations are complete.
},

onSubpageTransitionDone() {
    // sub nav/tab page transitions are complete.
}

Đây là những nội dung bổ sung hữu ích giúp trì hoãn công việc và giảm thiểu hiện tượng giật giữa các lần chuyển đổi trang. Chúng ta sẽ nói thêm về điều này ở phần sau.

Làm khô chức năng phổ biến trên các trang

Tính kế thừa là một tính năng mạnh mẽ của Phần tử tuỳ chỉnh. Công cụ này cung cấp mô hình kế thừa chuẩn cho web.

Rất tiếc, Polymer 1.0 chưa triển khai tính kế thừa của phần tử tại thời điểm viết bài. Trong thời gian chờ đợi, tính năng Hành vi của Polymer cũng hữu ích không kém. Hành vi chỉ là sự hỗn hợp.

Thay vì tạo cùng một nền tảng API trên tất cả các trang, bạn nên DRY cơ sở mã bằng cách tạo các kết hợp dữ liệu dùng chung. Ví dụ: PageBehavior xác định các thuộc tính/phương thức phổ biến mà tất cả các trang trong ứng dụng cần có:

PageBehavior.html

let PageBehavior = {

    // Common properties all pages need.
    properties: {
    name: { type: String }, // Slug name of the page.
    ...
    },

    attached() {
    // If the page defines a `onPageTransitionDone`, call it when the router
    // fires 'page-transition-done'.
    if (this.onPageTransitionDone) {
        this.listen(document.body, 'page-transition-done', 'onPageTransitionDone');
    }

    // Update page meta data when new page is navigated to.
    document.body.id = `page-${this.name}`;
    document.title = this.title || 'Google I/O 2016';

    // Scroll to top of new page.
    if (IOWA.Elements.Scroller) {
        IOWA.Elements.Scroller.scrollTop = 0;
    }

    this.setupSubnavEffects();
    },

    detached() {
    this.unlisten(document.body, 'page-transition-done', 'onPageTransitionDone');
    this.teardownSubnavEffects();
    }
};

IOWA.IOBehaviors = IOWA.IOBehaviors || {PageBehavior: PageBehavior};

Như bạn có thể thấy, PageBehavior thực hiện các tác vụ phổ biến chạy khi một trang mới được truy cập. Những việc như cập nhật document.title, đặt lại vị trí cuộn và thiết lập trình nghe sự kiện cho hiệu ứng cuộn và điều hướng phụ.

Các trang riêng lẻ sử dụng PageBehavior bằng cách tải dưới dạng phần phụ thuộc và sử dụng behaviors. Bạn cũng có thể thoải mái ghi đè các thuộc tính/phương thức cơ sở nếu cần. Ví dụ: dưới đây là những gì "lớp con" trên trang chủ của chúng tôi ghi đè:

io-home-page.html

<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="PageBehavior.html">
<!-- rest of the import dependencies used by the page. -->

<dom-module id="io-home-page">
    <template>
    <!-- PAGE'S MARKUP -->
    </template>
    <script>
    Polymer({
        is: 'io-home-page',

        behaviors: [IOBehaviors.PageBehavior], // All pages have common functionality.

        // Pages define their own title and slug for the router.
        title: 'Schedule - Google I/O 2016',
        name: 'home',

        // The home page has custom setup work when it's added navigated to.
        // Note: PageBehavior's attached also gets called.
        attached() {
        if (this.app.isPhoneSize) {
            this.listen(IOWA.Elements.ScrollContainer, 'scroll', '_onPageScroll');
        }
        },

        // The home page does its own cleanup when a new page is navigated to.
        // Note: PageBehavior's detached also gets called.
        detached() {
        this.unlisten(IOWA.Elements.ScrollContainer, 'scroll', '_onPageScroll');
        },

        // The home page can define onPageTransitionDone to do extra work
        // when page transitions are done, and thus preventing janky animations.
        onPageTransitionDone() {
        ...
        }
    });
    </script>
</dom-module>

Chia sẻ kiểu

Để chia sẻ kiểu giữa nhiều thành phần trong ứng dụng, chúng ta đã sử dụng các mô-đun kiểu dùng chung của Polymer. Mô-đun kiểu cho phép bạn xác định một phần CSS một lần và sử dụng lại ở nhiều vị trí trong ứng dụng. Đối với chúng tôi, "các địa điểm khác nhau" có nghĩa là các thành phần khác nhau.

Trong IOWA, chúng tôi đã tạo shared-app-styles để chia sẻ màu sắc, kiểu chữ và lớp bố cục trên các trang và các thành phần khác mà chúng tôi tạo.

shared-app-styles.html

<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../bower_components/paper-styles/color.html">

<dom-module id="shared-app-styles">
    <template>
    <style>
        [layout] {
        @apply(--layout);
        }
        [layout][horizontal] {
        @apply(--layout-horizontal);
        }
        .scrollable {
        @apply(--layout-scroll);
        }
        .noscroll {
        overflow: hidden;
        }
        /* Style radio buttons and tabs the same throughout the app */
        paper-tabs {
        --paper-tabs-selection-bar-color: currentcolor;
        }
        paper-radio-button {
        --paper-radio-button-checked-color: var(--paper-cyan-600);
        --paper-radio-button-checked-ink-color: var(--paper-cyan-600);
        }
        ...
    </style>
    </template>
</dom-module>

io-home-page.html

<link rel="import" href="shared-app-styles.html">
<!-- Rest of import dependencies used by the page. -->

<dom-module id="io-home-page">
    <template>
    <style include="shared-app-styles">
        :host { display: block} /* Other element styles can go here. */
    </style>
    <!-- PAGE'S MARKUP -->
    </template>
    <script>Polymer({...});</script>
</dom-module>

Ở đây, <style include="shared-app-styles"></style> là cú pháp của Polymer để nói "thêm các kiểu vào mô-đun có tên là "shared-app-styles".

Chia sẻ trạng thái ứng dụng

Bây giờ, bạn đã biết rằng mỗi trang trong ứng dụng của chúng ta là một Phần tử tuỳ chỉnh. Tôi đã nói cả triệu lần rồi. Được thôi, nhưng nếu mỗi trang đều là thành phần web độc lập, thì có thể bạn đang thắc mắc về cách chúng tôi chia sẻ trạng thái trên ứng dụng.

IOWA sử dụng một kỹ thuật tương tự như kỹ thuật chèn phần phụ thuộc (Angular) hoặc redux (Phản hồi) để chia sẻ trạng thái. Chúng tôi đã tạo một tài sản app chung và treo các tài sản phụ dùng chung khỏi tài sản đó. app được truyền qua ứng dụng bằng cách chèn ứng dụng đó vào mọi thành phần cần dữ liệu. Việc sử dụng các tính năng liên kết dữ liệu của Polymer giúp việc này trở nên dễ dàng vì chúng ta có thể đi dây mà không cần viết bất cứ mã nào:

<lazy-pages>
    <template is="dom-if" name="home">
    <io-home-page date="2016-05-18T17:00:00Z" app="[[app]]"></io-home-page>
    </template>

    <template is="dom-if" name="schedule">
    <io-schedule-page date="2016-05-18T17:00:00Z" app="{ % templatetag openvariable % }app}}"></io-schedule-page>
    </template>
    ...
</lazy-pages>

<google-signin client-id="..." scopes="profile email"
                            user="{ % templatetag openvariable % }app.currentUser}}"></google-signin>

<iron-media-query query="(min-width:320px) and (max-width:768px)"
                                query-matches="{ % templatetag openvariable % }app.isPhoneSize}}"></iron-media-query>

Phần tử <google-signin> cập nhật thuộc tính user khi người dùng đăng nhập vào ứng dụng. Vì thuộc tính đó được liên kết với app.currentUser, nên mọi trang muốn truy cập vào người dùng hiện tại chỉ cần liên kết với app và đọc thuộc tính phụ currentUser. Bản thân kỹ thuật này rất hữu ích cho việc chia sẻ trạng thái trên ứng dụng. Tuy nhiên, một lợi ích khác là chúng tôi đã tạo phần tử đăng nhập một lần và sử dụng lại kết quả của phần tử đó trên trang web. Tương tự như vậy đối với các truy vấn về nội dung nghe nhìn. Việc mỗi trang sao chép trang đăng nhập hoặc tạo một nhóm truy vấn nội dung nghe nhìn riêng sẽ là vô cùng lãng phí. Thay vào đó, các thành phần chịu trách nhiệm về chức năng/dữ liệu trên toàn ứng dụng tồn tại ở cấp ứng dụng.

Hiệu ứng chuyển tiếp trang

Khi thao tác trong ứng dụng web Google I/O, bạn sẽ nhận thấy các hiệu ứng chuyển đổi mượt mà của các trang (à thiết kế material).

Hiệu ứng chuyển đổi trang của IOWA trong thực tế.
Quá trình chuyển đổi trang của IOWA trong thực tế.

Khi người dùng chuyển đến một trang mới, một trình tự sẽ xảy ra:

  1. Thanh điều hướng trên cùng trượt thanh lựa chọn đến đường liên kết mới.
  2. Tiêu đề của trang này biến mất.
  3. Nội dung của trang trượt xuống rồi biến mất.
  4. Bằng cách đảo ngược những ảnh động đó, tiêu đề và nội dung của trang mới sẽ xuất hiện.
  5. (Không bắt buộc) Trang mới thực hiện công việc khởi tạo bổ sung.

Một trong những thách thức của chúng tôi là tìm ra cách tạo ra bước chuyển đổi mượt mà mà không làm giảm hiệu suất. Có rất nhiều việc phải làm nên bữa tiệc không được chào đón hiện tượng giật. Giải pháp của chúng tôi là sự kết hợp giữa Web Animations API và Promises. Việc sử dụng kết hợp cả hai tính năng này mang lại cho chúng tôi tính linh hoạt, hệ thống ảnh động cắm và phát cũng như quyền kiểm soát chi tiết để giảm thiểu hiện tượng giật da.

Cách hoạt động

Khi người dùng nhấp vào một trang mới (hoặc nhấn lùi/đi tới), runPageTransition() của bộ định tuyến sẽ thực hiện phép thuật của mình bằng cách chạy qua một loạt các Promise (Lời hứa). Việc sử dụng Promises cho phép chúng tôi sắp xếp các ảnh động một cách cẩn thận và giúp hợp lý hoá trạng thái "không đồng bộ" của Ảnh động CSS cũng như việc tải nội dung một cách linh động.

class Router {

    init() {
    window.addEventListener('popstate', e => this.runPageTransition());
    }

    runPageTransition() {
    let endPage = this.state.end.page;

    this.fire('page-transition-start');              // 1. Let current page know it's starting.

    IOWA.PageAnimation.runExitAnimation()            // 2. Play exist animation sequence.
        .then(() => {
        IOWA.Elements.LazyPages.selected = endPage;  // 3. Activate new page in <lazy-pages>.
        this.state.current = this.parseUrl(this.state.end.href);
        })
        .then(() => IOWA.PageAnimation.runEnterAnimation())  // 4. Play entry animation sequence.
        .then(() => this.fire('page-transition-done')) // 5. Tell new page transitions are done.
        .catch(e => IOWA.Util.reportError(e));
    }

}

Thu hồi từ phần "Keeping mọi thứ DRY: chức năng phổ biến trên các trang", các trang nghe sự kiện DOM page-transition-startpage-transition-done. Bây giờ, bạn sẽ biết nơi các sự kiện đó được kích hoạt.

Chúng ta đã sử dụng API Ảnh động trên web thay vì trình trợ giúp runEnterAnimation/runExitAnimation. Trong trường hợp runExitAnimation, chúng ta lấy một vài nút DOM (quảng cáo trên đầu trang chủ và vùng nội dung chính), khai báo điểm bắt đầu/kết thúc của mỗi ảnh động và tạo GroupEffect để chạy cả hai nút song song:

function runExitAnimation(section) {
    let main = section.querySelector('.slide-up');
    let masthead = section.querySelector('.masthead');

    let start = {transform: 'translate(0,0)', opacity: 1};
    let end = {transform: 'translate(0,-100px)', opacity: 0};
    let opts = {duration: 400, easing: 'cubic-bezier(.4, 0, .2, 1)'};
    let opts_delay = {duration: 400, delay: 200};

    return new GroupEffect([
    new KeyframeEffect(masthead, [start, end], opts),
    new KeyframeEffect(main, [{opacity: 1}, {opacity: 0}], opts_delay)
    ]);
}

Bạn chỉ cần sửa đổi mảng để chuyển đổi khung hiển thị chi tiết hơn (hoặc ít chi tiết hơn)!

Hiệu ứng cuộn

IOWA có một số hiệu ứng thú vị khi bạn cuộn trang. Đầu tiên là nút hành động nổi (FAB). Nút này đưa người dùng trở lại đầu trang:

    <a href="#" tabindex="-1" aria-hidden="true" aria-label="back to top" onclick="backToTop">
      <paper-fab icon="io:expand-less" noink tabindex="-1"></paper-fab>
    </a>

Tính năng cuộn mượt được triển khai bằng phần tử bố cục ứng dụng của Polymer. Chúng cung cấp các hiệu ứng cuộn ngay lập tức như thanh điều hướng cố định/quay lại trên cùng, bóng đổ, chuyển đổi màu sắc và nền, hiệu ứng thị sai và cuộn mượt mà.

    // Smooth scrolling the back to top FAB.
    function backToTop(e) {
      e.preventDefault();

      Polymer.AppLayout.scroll({top: 0, behavior: 'smooth',
                                target: document.documentElement});

      e.target.blur();  // Kick focus back to the page so user starts from the top of the doc.
    }

Một vị trí khác chúng tôi sử dụng các phần tử <app-layout> là dành cho thanh điều hướng cố định. Như bạn có thể thấy trong video, thẻ này sẽ biến mất khi người dùng cuộn trang xuống và quay lại khi cuộn lên lại.

Thành phần điều hướng cuộn cố định
Thành phần điều hướng cuộn cố định sử dụng .

Chúng tôi sử dụng phần tử <app-header> gần như nguyên trạng. Rất dễ để thả vào và sử dụng các hiệu ứng cuộn độc đáo trong ứng dụng. Chắc chắn rồi, chúng ta có thể tự triển khai các phần tử đó, nhưng việc sử dụng các chi tiết đã được mã hoá vào một thành phần có thể sử dụng lại sẽ giúp tiết kiệm rất nhiều thời gian.

Khai báo phần tử. Tuỳ chỉnh bằng các thuộc tính. Bạn đã hoàn tất!

    <app-header reveals condenses effects="fade-background waterfall"></app-header>

Kết luận

Đối với ứng dụng web tiến bộ I/O, chúng tôi có thể xây dựng toàn bộ giao diện người dùng trong vài tuần nhờ các thành phần web và tiện ích thiết kế Material Design được tạo sẵn của Polymer. Các tính năng của API gốc (Thành phần tuỳ chỉnh, DOM bóng, <template>) phù hợp một cách tự nhiên với tính linh động của SPA. Việc tái sử dụng giúp tiết kiệm rất nhiều thời gian.

Nếu bạn muốn tạo một ứng dụng web tiến bộ của riêng mình, hãy tham khảo App Toolbox. Hộp công cụ ứng dụng của Polymer là một tập hợp các thành phần, công cụ và mẫu để xây dựng PWA bằng Polymer. Đây là một cách dễ dàng để thiết lập và sử dụng.