在 PWA 中使用 App Shell 模型提升性能和用戶感知體驗

作者|潘宇琪
編輯|Daisy

在構建 PWA 應用時,使用 App Shell 模型能夠在視覺和首屏加載速度方面帶來用戶體驗的提升。另外,在配合 Service Worker 離線緩存之后,用戶在后續訪問中將得到快速可靠的瀏覽體驗。 在實踐過程中,借助流行框架與構建工具提供的眾多特性,我們能夠在項目中便捷地實現 App Shell 模型及其緩存方案。最后,在常見的 SPA 項目中,我們試圖使用 Skeleton 方案進一步提升用戶的感知體驗。

App Shell 模型

相比 Native App,PWA 有以下優勢:

  • Linkable 畢竟是 Web 站點,通過鏈接跳轉,也便于分享以及索引。
  • Progressive 漸進式提升站點體驗。即使不支持 Service Worker 仍能運行。添加到主屏,消息推送等特性也是如此。

我們都很熟悉 Native App 中常見的 Shell 展示效果,通常快速加載應用的簡單 UI (頂部導航條,側邊欄,Loading 動畫等)并緩存,后續訪問甚至是離線狀態仍能立即展示,而頁面實際內容動態加載。PWA 在保持以上優勢的基礎上,也可以借鑒這一方案以提升性能和用戶感知體驗,這就是 App Shell 模型。
這里寫圖片描述
我們對于 PWA 中的 App Shell 模型的大致總結:

  • 內容上是由 HTML CSS 和 JS 組成的資源集合
  • 還需要負責后續動態加載頁面實際內容
  • 與iOS/Android App 相比,體積小得多

那么在具體項目中應該如何應用這一模型呢?或者說,對于已有項目的改造成本有多大呢?

我們熟悉的 Web 項目的架構大致如下:

  • Server-side Rendering 首屏加載速度快,但是后續每次頁面間跳轉都需要重新下載全部資源。
  • Client-side Rendering 首屏加載速度慢,后續頁面跳轉迅速。

這里寫圖片描述
所以兩者結合可以得到最好的效果,首屏由 SSR 渲染,后續由 CSR 動態渲染頁面中部分內容,類似 SPA 的效果。 借助構建工具例如 Webpack 和前端框架(React Vue)提供的服務端渲染特性,同一套代碼在編譯后可以同時運行在雙端,這就是 Universal/Isomorphic 同構應用的思想。

在上述架構下都可以應用 App Shell 模型。首先我們來看在 SPA 中的應用。

SPA 中的應用

SPA 中的內容全部由 JS 在前端渲染。為了實現 App Shell 的特性,在具體實現或者對于已有項目的改造時,我們可以應用 PRPL 模式。

PRPL 模式

PRPL 模式是 Google 提出的,包含以下特性:

  • Push 推送 - 為初始網址路由推送關鍵資源。
  • Render 渲染 - 渲染初始路由。
  • Precache 預緩存 - 預緩存剩余路由。
  • Lazyload 延遲加載 - 延遲加載并按需創建剩余路由。

簡單用一張圖表示整個過程:
這里寫圖片描述
前面說過,App Shell 在內容上是由 HTML CSS 和 JS 組成的資源集合。為了保證這些資源的加載速度,必須精簡。 在這一思路下,它將包含:

  • SPA 唯一的一個 HTML。
  • JS 包括:渲染 UI 代碼,前端路由器,渲染初始路由內容代碼。
  • 關鍵路徑樣式,其他靜態資源。

為了實現全部或者部分特性,我們需要依賴以下技術:

  • HTTP/2 服務。盡早幫助瀏覽器發現靜態資源并加載。
  • 前端路由。能夠渲染初始路由,并且能支持后續動態加載并添加剩余路由。
  • Service Worker 預緩存后續所需路由文件及靜態資源。
  • 構建工具的支持。包括對于 HTTP/2 的 unbundle 支持,對于代碼分割的支持等等。

所幸現有的很多優秀工具和框架已經能幫助解決大部分問題,下面我們來看具體實現。

代碼分割

為了保證 App Shell 包含資源的精簡,需要將初始路由內容與后續路由內容分開。 在編譯時需要構建工具進行分割打包操作。在編寫代碼時,有兩種做法:

  • 代碼在編寫時就是物理分割好的
  • 代碼在編寫時不分割,使用特殊的語法指示構建工具在編譯時進行分割處理

對于第一種做法,我們以 Polymer 為例。由于使用了 HTML imports,需要分割的代碼天然就是物理分割,包含在各自 HTML 中的。 在構建時,配套的構建工具會讀取自身的配置文件 polymer.json,其中顯式指明了這三部分內容:

  • entrypoint 即項目的入口文件,應該足夠精簡,僅包含特性檢測之后引入的 polyfill
  • App Shell。App Shell 包含了前端路由,全局的導航 UI 等等,以及需要實現 動態加載 fragment 的邏輯。
  • fragment 類似異步路由組件。
    這里寫圖片描述
    而對于第二種做法,我們開發者最熟悉的例子就是 Webpack 了。 引入 babel-plugin-syntax-dynamic-import 插件,開發者就可以使用 dynamic-import 語法:
import(/* webpackChunkName: "my-view1" */ './my-view1').then((myView1) => {//...});

現在我們已經將初始路由內容與后續路由內容分開了,渲染內容將由路由負責。

路由支持

對于 PRPL 模式中的路由來說,除了負責初始路由的渲染,還需要支持后續動態加載并添加剩余路由。

Polymer 提供了異步引入的 API,供配套的路由使用。 這樣就能實現異步加載,并在出錯時跳轉到 404 頁面:

var resolvedPageUrl = this.resolveUrl('my-view1.html');
this.importHref(resolvedPageUrl,null,this._importFailedCallback,true
);

而在 Vue 中,由于框架本身就支持異步組件,在 vue-router 中很容易實現路由的懶加載:

import Index from './Index.vue';
const MyView1 = () => import('./MyView1.vue');
const router = new VueRouter({routes: [{ path: '/', component: Index }{ path: '/my-view1', component: MyView1 }]
});

React 也是一樣:

import Loadable from 'react-loadable';
import Loading from './Loading';const LoadableComponent = Loadable({loader: () => import('./MyView1.jsx'),loading: Loading,
})

這樣結合之前的代碼分割,我們就完成了初始路由的渲染,以及后續剩余路由的按需加載。

Service Worker 預緩存

雖然說實現了路由內容的按需加載,但畢竟要等到實際路由切換時才會請求相應代碼并執行。 如果能提前告知瀏覽器預取這部分資源,就可以提前完成掉網絡開銷。

首先能想到的一個方案是 ,瀏覽器在空閑時會去請求這些資源放入 HTTP 緩存:

<link rel="prefetch" href="image.png">

但是對于開發者而言,需要更精確地控制緩存,因此還是得使用 Service Worker。

在項目構建階段,將靜態資源列表(數組形式)及本次構建版本號注入 Service Worker 代碼中。 在 SW 運行時(Install 階段)依次發送請求獲取靜態資源列表中的資源(JS,CSS,HTML,IMG,FONT…),成功后放入緩存并進入下一階段(Activated)。這個在實際請求之前進行緩存的過程就是預緩存。

預緩存 App Shell 包含的 HTML JS 和 CSS,以及懶加載需要的路由 JS。

var filesToCache = ['/index.html’,'/js/main.js','/js/my-view1.js','/js/my-view2.js','/css/main.css'
];self.addEventListener('install', function(e) {e.waitUntil(caches.open(cacheName).then(function(cache) {return cache.addAll(filesToCache);}));
});

借助 Workbox 提供的命令行工具以及構建工具配套的插件,開發者能輕松地通過配置生成預緩存列表甚至是整個 Service Worker 文件,緩存的更新交給 Workbox 完成。除了預緩存,Workbox 還提供了一系列 API 幫助開發者管理動態緩存,使用默認離線頁面等等。

importScripts('./workbox-sw.prod.js');
importScripts('./precache-manifest.js');workbox.skipWaiting();
workbox.clientsClaim();workbox.precaching.precacheAndRoute(self.__precacheManifest);

推送關鍵資源

我們知道 HTTP/2 中,服務端在返回 HTML 的同時,可以向瀏覽器推送所需的靜態資源,這樣在瀏覽器解析 HTML 遇到相應的資源時,它們已經在 HTTP 緩存中了。所以針對這一特性,過去打包所有靜態資源以減少網絡請求數的考量就沒有必要了,反而拆分成多個 bundle 更有利于不同頁面間共享的緩存。

例如 twitter 的 mobile 站點,注意下載 HTML 和首屏需要的 JS 幾乎是同時進行的:
這里寫圖片描述

但是對于不支持 HTTP/2 的瀏覽器,還有 這種方式,考慮兼容性兩者可以同時使用。
這里寫圖片描述

SSR 中的應用

在 SPA 架構的應用中,App Shell 通常包含在 HTML 頁面中,連同頁面一并被預緩存,保證了離線可訪問。但是在 SSR 架構場景下,情況就不一樣了。所有頁面首屏均是服務端渲染,預緩存的頁面不再是有限且固定的。如果預緩存全部頁面,SW 需要發送大量請求不說,每個頁面都包含的 App Shell 部分都被重復緩存,也造成了緩存空間的浪費。

既然針對全部頁面的預緩存行不通,我們能不能將 App Shell 剝離出來,單獨緩存僅包含這個空殼的頁面呢?要實現這一點,就需要對后端模板進行修改,通過傳入參數控制返回包含 App Shell 的完整頁面 OR 代碼片段。這樣首屏使用完整頁面,而后續頁面切換交給前端路由完成,請求代碼片段進行填充。

通用思路

  1. 改造后端模板以支持返回完整頁面和內容片段( contentOnly )
  2. 服務端增加一條針對 App Shell 的路由規則,返回僅包含App Shell 的 HTML 頁面( shell.html )
  3. 預緩存 App Shell 頁面
  4. Service Worker 攔截所有 HTML 請求,統一返回緩存的 App Shell 頁面。
    同時向服務端請求當前頁面需要的內容片段并寫入緩存
  5. 前端路由( app.js )向服務端請求內容片段,發現緩存中已存在,將其填充進 App Shell 中,完成前端渲染

傳統后端模版項目

以傳統的后端模版項目為例,對于用戶的請求,根據 URL 使用默認 Layout + 對應視圖模版進行響應。
這里寫圖片描述
而 Service Worker 安裝時,也會向服務器發送請求。對于服務器而言,新增了一種訪問角色,與之對應的,需要增加一系列針對 Service Worker 的路由規則,將單獨的視圖模版和默認 Layout 返回給 Service Worker。
Service Worker 訪問服務器
對于用戶而言,在 Service Worker 安裝成功之后,對于 HTML 的請求都會被攔截,渲染模板的工作全部由 Service Worker 完成。
Service Worker 渲染模板
下面我們來看具體的示例代碼,如果使用類似 express 這樣的服務器:
服務器渲染示例
而在這樣的同構思路下,如果服務端代碼也是使用 Node.js 編寫,理想情況下 Service Worker 就能復用其中的模板渲染和路由邏輯。
Service Worker 渲染示例

App Shell 性能

知,一些 SDK 代碼等等)也可以進行懶加載,這樣可以大幅減少初始路由內容的大小。

我們以 Vue hackernews 2.0 這個同構項目為例,在沒有使用代碼分割的情況下,所有的業務邏輯全在 app.js 中。

在 3G 環境下,首屏加載時間約為 2.9s
原始狀態
使用代碼分割后,首屏不需要的業務邏輯從 app.js 中移動到了異步加載文件中。首屏加載時間約為 1.2s
路由級別的 Code Splitting
使用 Service Worker 預緩存之后,再次訪問速度極快,僅 0.2s
使用 Service Worker 后,再次訪問
首屏性能提升是很明顯的,但是還有優化空間嗎?

Skeleton 方案

在 SPA 中,在實際內容由 JS 渲染完成之前,會存在一段白屏時間。參考 Native App 中的通常做法,可以展示 Skeleton 骨架屏,相比一個簡單的 Loading 動畫,更能讓用戶感覺內容就快加載出來了。但需要注意在本質上,這和 Loading 是沒有區別的,也并不能減少白屏時間,僅僅是提高了一些用戶的感知體驗。
這里寫圖片描述

下面我們將從生成方式,不同路由間的差異性問題以及優化展現速度這三方面展開。

生成方式

從骨架屏包含的內容上看,與 Loading 一樣,都是由內聯在 HTML 中的樣式和 DOM 結構片段組成。 我們希望在構建階段自動將這些內容注入 HTML 中,在生成方式上有兩種:

  • 編寫額外組件
  • 自動根據頁面內容生成
    首先來看第一種,Skeleton 也可以視為一種組件,在編寫時與其他組件開發體驗一致。但不同于其他組件在運行時前端渲染,Skeleton 組件需要在構建時,也就是 Node.js 環境中渲染。借助框架的 SSR 方案,我們很容易配合構建工具實現。

插件大致實現如下:

  1. 在 Webpack 當前編譯環境中創建一個 childCompiler,繼承編譯上下文。這樣可以保證 Skeleton
    1. 使用框架提供的 SSR 方案渲染 Skeleton 組件,得到對應的HTML 片段
    2. 使用插件分離樣式,得到 CSS
    3. 注入 HTML 中

這種方案存在兩個問題:

  1. 由于依賴框架的 SSR 方案,針對不同的框架需要開發不同的插件。目前我開發了 vue-skeleton-webpack-plugin 和 react-skeleton-webpack-plugin。
  2. 需要手動編寫 Skeleton 組件。
    而在第二種方案中,不需要開發者編寫額外的 Skeleton 組件,既然骨架屏是要反映頁面內容的大致框架,完全可以在真實頁面基礎上,將內容替換成占位元素得到最終效果。Eleme 團隊的 page-skeleton-webpack-plugin 就是這樣一款優秀的插件。

插件大致實現如下:

  1. 使用 puppeteer 提供的 API 在 Node.js
  2. 環境中運行 headless Chrome 打開需要生成 Skeleton 的頁面
  3. 注入樣式,將不同的元素替換成占位符
  4. 獲取頁面樣式和 HTML 片段
  5. 注入 HTML 中

根據路由展示

以上兩種生成方式都會面臨同樣的一個問題,那就是 SPA 中如果只生成一份 Skeleton,如何能保證匹配不同的路由頁面呢? 在試圖用一個 Skeleton 匹配多個差別極大的路由頁面時,往往就退化成了 Loading 方案。

所以我們可以在構建時,為幾個重要的路由頁面生成各自的骨架屏,在 HTML 中注入一小段 JS,根據當前路由路徑控制展示某一個。 大致思路如下:

<div id="skeleton1" style="display:none">...</div>
<div id="skeleton2" style="display:none">...</div>
<script>// 根據路由展示對應 skeleton
</script>

總結

無論是 SPA 下的 PRPL 模式,還是 SSR 下的同構思路,靈活運用其中的技術思路,借助 App Shell 模型,成熟的框架以及構建工具,相信一定能開發出更多高質量的 PWA 應用。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/535865.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/535865.shtml
英文地址,請注明出處:http://en.pswp.cn/news/535865.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【轉】超酷的 mip-infinitescroll 無限滾動(無限下拉)

寫在前面 無限滾動技術&#xff08;又叫做無限下拉技術&#xff09;被廣泛應用于新聞類&#xff0c;圖片預覽類網站。對用戶來講&#xff0c;使用無限滾動的頁面有源源不斷的信息可以預覽&#xff0c;增加用戶在頁面的停留時長。技術上原理也很簡單&#xff0c;在頁面加載時加…

日常問題——Mac下新建目錄報Read-only file system

問題描述&#xff1a; 今天在根目錄下&#xff0c;新建目錄時出現了Read-only file system提示為只讀的錯誤。電腦最近并沒有非正常關機之類可能導致文件損傷的操作&#xff0c;但是最近倒是進行了一次系統更新。 解決方案&#xff08;過程&#xff09;&#xff1a; 從系統更…

MongoDB(二):MongoDB的安裝

這里以OSX系統為例&#xff0c;window和linux可以參考https://www.runoob.com/mongodb/mongodb-linux-install.html 1、我們使用 curl 命令來下載安裝&#xff1a; # 進入 /usr/local cd /usr/local# 下載 sudo curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_…

百度推出 MIP Baidu Path鏈接

在站長將站點 MIP 化時&#xff0c;需要關注 URL 的一共有三個&#xff1a;MIP URL, MIP-Cache URL 以及 MIP Baidu Path。 從 URL 說起 在互聯網中&#xff0c;URL 定義頁面的地址&#xff0c;每個 URL 對應一個頁面。而 MIP URL 則是 MIP 頁的原始地址&#xff0c;指向托管…

Postman接口測試(超詳細整理)

常用的接口測試工具主要有以下幾種 Postman&#xff1a;簡單方便的接口調試工具&#xff0c;便于分享和協作。具有接口調試&#xff0c;接口集管理&#xff0c;環境配置&#xff0c;參數化&#xff0c;斷言&#xff0c;批量執行&#xff0c;錄制接口&#xff0c;Mock Server, …

mip-link 組件功能升級說明

背景描述 某個頁面被多少頁面引用&#xff08;在其他頁面上有指向這個頁面的 a 標簽&#xff09;&#xff0c;是搜索引擎判斷這個頁面價值的其中一個因子。這里的搜索引擎不只是指百度&#xff0c;還包括國內外其他的搜索引擎。 MIP 在最初設計 MIP url 跳轉邏輯實現時&#…

日常問題——使用Xshell 連接虛擬機報錯 Disconnected from remote host

問題描述&#xff1a; 使用Xshell進行連接虛擬機的操作時出現了Disconnected from remote host的錯誤&#xff01; 解決方案&#xff08;過程&#xff09;&#xff1a; 1、vim /etc/ssh/sshd_config 2、#UseDNS yes改為UseDNS no 3、重啟service sshd restart 問題解決&…

【轉】AB實驗設計思路及實驗落地

這篇文章會討論&#xff1a; 1. 在什么情況下需要做 AB 實驗 2. 從產品/交互角度&#xff0c;如何設計一個實驗 3. 前端工程師如何打點 4. 如何統計數據&#xff0c;并保證數據準確可信 5. 如何分析實驗數據&#xff0c;有哪些數據需要重點關注 6. 附&#xff1a;如何搭建…

簡單實現MySQL數據實時增量同步到Kafka————Maxwell

任務需求&#xff1a;將MySQL里的數據實時增量同步到Kafka 1、準備工作 1.1、MySQL方面&#xff1a;開啟BinLog 1.1.1、修改my.cnf文件 vi /etc/my.cnf [mysqld] server-id 1 binlog_format ROW1.1.2、重啟MySQL,然后登陸到MySQL之后&#xff0c;查看是否已經修改過來: …

【轉】mip-semi-fixed 走走又停停

寫在前面 MIP 中懸浮元素的特殊情況 其實組件上線已經有一段時間了&#xff0c;最開始看到這個需求是站長提交了一個這中功能的組件過來&#xff0c;不過看過代碼立刻就想到了 MIP 頁面的特殊性&#xff1a;從結果頁打開的 MIP 頁面&#xff0c;是嵌套在一個 iframe 之中的。…

Mac使用Homebrew安裝Kafka

1、使用brew install命令安裝Kafka $ brew install kafka安裝過程將依賴安裝 zookeeper軟件位置 /usr/local/Cellar/zookeeper /usr/local/Cellar/kafka配置文件位置 /usr/local/etc/kafka/zookeeper.properties /usr/local/etc/kafka/server.properties 備注&#xff1a;后…

廣州站長沙龍 MIP 問題及答案

1. mip提交幾個月時間了&#xff0c;生效量比較少&#xff0c;是什么原因&#xff1f; 答&#xff1a;提交 MIP 頁面后&#xff0c;經過收錄、校驗、和生效三個步驟&#xff0c;才能在結果頁看到閃電標。 1&#xff09;提交 URL 后&#xff0c;spider 會去抓取收錄&#xff1…

日常問題——初始化Hive倉庫報錯com.google.common.base.Preconditions.checkArgument

問題描述&#xff1a; 初始化Hive倉庫報錯Exception in thread “main” java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V 解決方案&#xff08;過程&#xff09;&#xff1a; com.google.commo…

【轉】百度站長平臺MIP引入工具使用心得

MIP引入主動推送流程 對于 MIP 站點改造好了&#xff0c;我們如何提交數據&#xff0c;并且 MIP 提交后&#xff0c;我們能得到哪些數據的反饋&#xff0c;在這里簡單的寫一篇文章&#xff0c;說一下。 改造 MIP&#xff0c;我們一般是添加了一個二級域名站點進行改造&#x…

Hadoop之HDFS應用

1、通過http://127.0.0.1:8088/即可查看集群所有節點狀態&#xff1a; 2、訪問http://localhost:9870/即可查看文件管理頁面&#xff08;在3.0.0中在之前的版本中文件管理的端口是50070&#xff0c;替換為了9870端口&#xff09;&#xff1a; ————進入文件系統 ————…

MIP ACCESS細節剖析

什么是 MIP ACCESS MIP ACCESS 由百度 MIP 團隊開發的一種頁面訪問權限控制機制&#xff0c;能夠允許網頁發布者在頁面元素中定義內容標記&#xff0c;并結合用戶訪問情況進行綜合評價&#xff0c;從而展現或隱藏頁面中內容&#xff0c;直至用戶登錄、訂閱或付費后才能夠查看隱…

HDFS常用Shell命令

1、-ls: 顯示目錄信息 hadoop fs -ls /2、-mkdir&#xff1a;在HDFS上創建目錄 hadoop fs -mkdir -p /demo/test3、-moveFromLocal&#xff1a;從本地剪切粘貼到HDFS hadoop fs -moveFromLocal a.txt /demo/test/a.txt4、-appendToFile&#xff1a;追加一個文件到已經存在…

Linux環境下Flume的安裝

1、在官網http://flume.apache.org/download.html下載flume的壓縮包 2、解壓到指定位置并重命名 tar -zxvf apache-flume-1.9.0-bin.tar.gz3、配置環境并生效 #vi ~/.bashrc export FLUME_HOME/usr/local/APP/flume export PATH$PATH:$FLUME_HOME/bin #使變量設置生效 #sour…

MIPCache 域名升級

一、MIPCache URL 是什么 舉個例子&#xff0c;MIP 官網的 URL 為&#xff1a; https://www.mipengine.org 對應的 MIPCache 的 URL 為&#xff1a; https://mipcache.bdstatic.com/c/s/www.mipengine.org 所謂 MIPCache URL 是經過 MIP-Cache CDN 緩存后的 MIP 頁面地址&…

Flume監聽端口,輸出端口數據案例

1、在flume目錄下新建/myconf目錄,并在目錄下新建socket-console.conf 文件&#xff01; mkdir myconf cd myconf touch socket-console.conf2、編輯文件vim socket-console.conf&#xff0c;添加以下內容&#xff1a; # 定義這個agent中各組件的名字 a1.sources r1 a1.sink…